diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index f11894d33..0faf8a866 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -29,6 +29,15 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Cache turbo
+ uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf
+ with:
+ path: .turbo
+ key: ${{ runner.os }}-turbo-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-turbo-
+
- name: Install pnpm
run: npm install -g pnpm@9
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
@@ -42,6 +51,6 @@ jobs:
if: matrix.os == 'macos-latest'
run: npm install -g appdmg
- name: Install dependencies
- run: pnpm install
+ run: npm exec turbo run bootstrap
- name: Run e2e
- run: cd apps/ui-tars && pnpm run test:e2e
+ run: npm exec turbo run ui-tars-desktop#test:e2e
diff --git a/.github/workflows/release-agent-tars.yml b/.github/workflows/release-agent-tars.yml
new file mode 100644
index 000000000..6c2bdf665
--- /dev/null
+++ b/.github/workflows/release-agent-tars.yml
@@ -0,0 +1,98 @@
+name: Release Agent-TARS app
+on:
+ workflow_dispatch:
+
+env:
+ CI: true
+ NODE_OPTIONS: --max-old-space-size=8192
+ HUSKY: 0
+
+permissions:
+ id-token: write
+ contents: write
+ attestations: write
+
+jobs:
+ publish_on_mac:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Show operating system info
+ run: |
+ echo "Operating System:"
+ uname -a
+ - name: Install pnpm
+ run: npm install -g pnpm@9
+ - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install Python setuptools
+ run: brew install python-setuptools
+ - name: Install appdmg
+ run: npm install -g appdmg
+ - name: Install the Apple certificate and provisioning profile
+ env:
+ BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
+ P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
+ BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
+ KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ run: |
+ # create variables
+ CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
+ PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
+
+ # import certificate and provisioning profile from secrets
+ echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
+ echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
+
+ # create temporary keychain
+ security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
+ security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
+
+ # import certificate to keychain
+ security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
+ security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
+ security list-keychain -d user -s $KEYCHAIN_PATH
+
+ # apply provisioning profile
+ mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
+ cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
+ - name: install dependencies
+ run: pnpm install
+ - name: publish Mac Universal
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ cd apps/agent-tars && pnpm run publish:mac
+
+ # publish_on_win:
+ # runs-on: windows-latest
+ # steps:
+ # - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ # - name: Show operating system info
+ # run: |
+ # echo "Operating System:"
+ # cmd /c ver
+ # - name: Install pnpm
+ # run: npm install -g pnpm@9
+ # - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
+ # with:
+ # node-version: 20
+ # cache: 'pnpm'
+ # - name: install dependencies
+ # run: pnpm install
+ # - name: publish
+ # env:
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # run: cd apps/agent-tars && pnpm run publish:win32
diff --git a/.github/workflows/release.yml b/.github/workflows/release-ui-tars.yml
similarity index 99%
rename from .github/workflows/release.yml
rename to .github/workflows/release-ui-tars.yml
index 18051682f..c6a0a0184 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release-ui-tars.yml
@@ -1,4 +1,4 @@
-name: Release app
+name: Release UI-TARS app
on:
# push:
# branches:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3bf6d0141..66aa8fcd4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -28,6 +28,15 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Cache turbo
+ uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf
+ with:
+ path: .turbo
+ key: ${{ runner.os }}-turbo-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-turbo-
+
- name: Install pnpm
run: npm install -g pnpm@9
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
@@ -35,11 +44,11 @@ jobs:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
- run: pnpm install
+ run: npm exec turbo run bootstrap
- name: Run typecheck
- run: cd apps/ui-tars && pnpm run typecheck
+ run: npm exec turbo run typecheck
- name: Run test
- run: pnpm run coverage
+ run: npm exec turbo run coverage
- name: Upload coverage data
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
with:
diff --git a/.gitignore b/.gitignore
index f47f3bf33..140fd2317 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,11 @@
node_modules
dist
out
+.tmp
.DS_Store
*.log*
.eslintcache
.env
test-results/
coverage/
+.turbo/
diff --git a/.prettierignore b/.prettierignore
index 9c6b791d5..ed170b57c 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,3 +4,4 @@ pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json
+apps/agent-tars/src/vendor/chat-ui/index.js
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..df814a23d
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,127 @@
+# Contributor covenant code of conduct
+
+## Our pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+- Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1b9b86092..6e4abf460 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,5 +1,7 @@
# Contributing to UI-TARS Desktop
+
+
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
@@ -55,30 +57,41 @@ This is a [Monorepo](https://pnpm.io/workspaces) project including the following
```bash
.
├── README.md
-├── package.json # Electron application dependencies
-├── forge.config.ts # Electron pack and publish configuration
-├── electron.vite.config.ts # Electron bundle configuration
-│
-├── src # Electron application source code
-│ ├── main # Main process source code(Like backend)
-│ ├── preload # Preload script source code
-│ └── renderer # Renderer process source code(Like frontend)
-│
-├── packages # Packages or Modules or SDK for UI-TARS Desktop
-│ ├── action-parser # Action parser for parsing UI-TARS model output into actions
-│ ├── core # Core SDK package for UI-TARS Agent
-│ ├── electron-ipc # Electron IPC for communication between main and renderer processes
-│ ├── shared # Shared code of the project(including types, utils, constants, etc.)
-│ ├── utio # UTIO (UI-TARS Insights and Observation)
-│ ├── visualizer # Sharing HTML Visualization Reporter
-│ └── operators # Automation operators
-│ ├── browserbase # Browserbase integration
-│ └── nut-js # Nut.js integration
-│
-├── docs # Documentation of the project
-├── rfcs # RFCs (Request for Comments) for the project
-├── e2e # E2E test cases for the project
-├── playwright.config.ts # E2E test configuration
+├── apps
+│ ├── agent-tars
+│ │ ├── src
+│ │ │ ├── main
+│ │ │ ├── preload
+│ │ │ ├── renderer
+│ │ │ └── vendor
+│ └── ui-tars
+│ └── src
+│ ├── main
+│ ├── preload
+│ └── renderer
+│
+├── packages
+│ ├── agent-infra
+│ │ ├── browser
+│ │ ├── browser-use
+│ │ ├── logger
+│ │ ├── mcp-client
+│ │ ├── mcp-servers
+│ │ ├── search
+│ │ └── shared
+│ ├── common
+│ │ ├── configs
+│ │ └── electron-build
+│ └── ui-tars
+│ ├── action-parser
+│ ├── cli
+│ ├── electron-ipc
+│ ├── operators
+│ ├── sdk
+│ ├── shared
+│ ├── tsconfig.node.json
+│ ├── utio
+│ └── visualizer
└── vitest.*.mts # Unit test configuration
```
@@ -103,7 +116,8 @@ $ pnpm install
#### Run the application
```bash
-$ pnpm run dev
+$ pnpm run dev:ui-tars # Start UI-TARS Desktop
+$ pnpm run dev:agent-tars # Start Agent-TARS Desktop
```
After the application starts, you can see the UI-TARS interface within the application.
diff --git a/README.md b/README.md
index e8d6471da..cf0f5172c 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,10 @@
+
+> [!IMPORTANT]
+> **\[2025-03-16\]** We released a **technical preview** version of a new desktop app - [Agent TARS](./apps/agent-tars/README.md), a multimodal AI agent that leverages browser operations by visually interpreting web pages and seamlessly integrating with command lines and file systems.
+
-
+
# UI-TARS Desktop
@@ -18,19 +22,6 @@ UI-TARS Desktop is a GUI Agent application based on [UI-TARS (Vision-Language Mo
|    👓 Midscene (use in browser)
-### ⚠️ Important Announcement: GGUF Model Performance
-
-The **GGUF model** has undergone quantization, but unfortunately, its performance cannot be guaranteed. As a result, we have decided to **downgrade** it.
-
-💡 **Alternative Solution**:
-You can use **[Cloud Deployment](#cloud-deployment)** or **[Local Deployment [vLLM]](#local-deployment-vllm)**(If you have enough GPU resources) instead.
-
-We appreciate your understanding and patience as we work to ensure the best possible experience.
-
-## Updates
-
-- 🚀 01.25: We updated the **[Cloud Deployment](#cloud-deployment)** section in the 中文版: [GUI模型部署教程](https://bytedance.sg.larkoffice.com/docx/TCcudYwyIox5vyxiSDLlgIsTgWf#U94rdCxzBoJMLex38NPlHL21gNb) with new information related to the ModelScope platform. You can now use the ModelScope platform for deployment.
-
## Showcases
| Instruction | Video |
@@ -38,6 +29,13 @@ We appreciate your understanding and patience as we work to ensure the best poss
| Get the current weather in SF using the web browser | |
| Send a twitter with the content "hello world" | |
+
+## News
+
+- **\[2025-02-20\]** - 📦 Introduced [UI TARS SDK](./docs/sdk.md), is a powerful cross-platform toolkit for building GUI automation agents.
+- **\[2025-01-23\]** - 🚀 We updated the **[Cloud Deployment](./docs/deployment.md#cloud-deployment)** section in the 中文版: [GUI模型部署教程](https://bytedance.sg.larkoffice.com/docx/TCcudYwyIox5vyxiSDLlgIsTgWf#U94rdCxzBoJMLex38NPlHL21gNb) with new information related to the ModelScope platform. You can now use the ModelScope platform for deployment.
+
+
## Features
- 🤖 Natural language control powered by Vision-Language Model
@@ -49,95 +47,19 @@ We appreciate your understanding and patience as we work to ensure the best poss
## Quick Start
-### Download
-
-You can download the [latest release](https://github.com/bytedance/UI-TARS-desktop/releases/latest) version of UI-TARS Desktop from our releases page.
-
-> **Note**: If you have [Homebrew](https://brew.sh/) installed, you can install UI-TARS Desktop by running the following command:
-> ```bash
-> brew install --cask ui-tars
-> ```
-
-### Install
-
-#### MacOS
-
-1. Drag **UI TARS** application into the **Applications** folder
-
-
-2. Enable the permission of **UI TARS** in MacOS:
- - System Settings -> Privacy & Security -> **Accessibility**
- - System Settings -> Privacy & Security -> **Screen Recording**
-
-
-3. Then open **UI TARS** application, you can see the following interface:
-
-
-
-#### Windows
-
-**Still to run** the application, you can see the following interface:
-
-
-
-### Deployment
-
-#### Cloud Deployment
-We recommend using HuggingFace Inference Endpoints for fast deployment.
-We provide two docs for users to refer:
-
-English version: [GUI Model Deployment Guide](https://juniper-switch-f10.notion.site/GUI-Model-Deployment-Guide-17b5350241e280058e98cea60317de71)
-
-中文版: [GUI模型部署教程](https://bytedance.sg.larkoffice.com/docx/TCcudYwyIox5vyxiSDLlgIsTgWf#U94rdCxzBoJMLex38NPlHL21gNb)
-
-#### Local Deployment [vLLM]
-We recommend using vLLM for fast deployment and inference. You need to use `vllm>=0.6.1`.
-```bash
-pip install -U transformers
-VLLM_VERSION=0.6.6
-CUDA_VERSION=cu124
-pip install vllm==${VLLM_VERSION} --extra-index-url https://download.pytorch.org/whl/${CUDA_VERSION}
-
-```
-##### Download the Model
-We provide three model sizes on Hugging Face: **2B**, **7B**, and **72B**. To achieve the best performance, we recommend using the **7B-DPO** or **72B-DPO** model (based on your hardware configuration):
-
-- [2B-SFT](https://huggingface.co/bytedance-research/UI-TARS-2B-SFT)
-- [7B-SFT](https://huggingface.co/bytedance-research/UI-TARS-7B-SFT)
-- [7B-DPO](https://huggingface.co/bytedance-research/UI-TARS-7B-DPO)
-- [72B-SFT](https://huggingface.co/bytedance-research/UI-TARS-72B-SFT)
-- [72B-DPO](https://huggingface.co/bytedance-research/UI-TARS-72B-DPO)
-
-
-##### Start an OpenAI API Service
-Run the command below to start an OpenAI-compatible API service:
-
-```bash
-python -m vllm.entrypoints.openai.api_server --served-model-name ui-tars --model
-```
-
-##### Input your API information
-
-
-
-
+## Deployment
-> **Note**: VLM Base Url is OpenAI compatible API endpoints (see [OpenAI API protocol document](https://platform.openai.com/docs/guides/vision/uploading-base-64-encoded-images) for more details).
+See [Deployment](./docs/deployment.md).
## Contributing
-[CONTRIBUTING.md](./CONTRIBUTING.md)
+See [CONTRIBUTING.md](./CONTRIBUTING.md).
-## SDK(Experimental)
+## SDK (Experimental)
-[SDK](./docs/sdk.md)
+See [@ui-tars/sdk](./docs/sdk.md)
## License
diff --git a/apps/agent-tars/CONTRIBUTIND.md b/apps/agent-tars/CONTRIBUTIND.md
new file mode 100644
index 000000000..9147e5f83
--- /dev/null
+++ b/apps/agent-tars/CONTRIBUTIND.md
@@ -0,0 +1,217 @@
+# Contributing to UI-TARS Desktop
+
+
+
+First off, thanks for taking the time to contribute! ❤️
+
+All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
+
+> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
+> - Star the project
+> - Tweet about it
+> - Refer this project in your project's readme
+> - Mention the project at local meetups and tell your friends/colleagues
+
+
+## I Have a Question / Bug Report
+
+> If you want to ask a question or report a bug, we assume that you have read the available Documentation.
+
+Before you ask a question, it is best to search for existing [Issues](https://github.com/bytedance/ui-tars-desktop/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
+
+If you then still feel the need to ask a question and need clarification, we recommend the following:
+
+- Open an [Issue](https://github.com/bytedance/ui-tars-desktop/issues/new).
+- Provide as much context as you can about what you're running into.
+- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
+
+We will then take care of the issue as soon as possible.
+
+## I Want To Contribute
+
+### Prerequisites
+
+- [Node.js](https://nodejs.org/en/download/) >= 20
+- [pnpm](https://pnpm.io/installation) >= 9
+
+#### Technology Stack
+
+This is a [Monorepo](https://pnpm.io/workspaces) project including the following technologies:
+
+- Cross-platform framework: [Electron](https://www.electronjs.org/)
+- Interface:
+ - [React](https://react.dev/)
+ - [Vite](https://vitejs.dev/)
+ - [Chakra UI V2](https://v2.chakra-ui.com/)
+- State management and communication:
+ - [Zustand](https://zustand.docs.pmnd.rs/)
+ - [@ui-tars/electron-ipc](https://github.com/bytedance/ui-tars-desktop/tree/main/packages/electron-ipc)
+- Automation framework/toolkit:
+ - [nut.js](https://nutjs.dev/)
+- Test framework
+ - [Vitest](https://vitest.dev/)
+ - [Playwright](https://playwright.dev/)
+
+### Structure of the project
+
+```bash
+.
+├── README.md
+├── apps
+│ ├── agent-infra
+│ │ ├── src
+│ │ │ ├── main
+│ │ │ ├── preload
+│ │ │ ├── renderer
+│ │ │ └── vendor
+│ └── ui-tars
+│ └── src
+│ ├── main
+│ ├── preload
+│ └── renderer
+│
+├── packages
+│ ├── agent-infra
+│ │ ├── browser
+│ │ ├── browser-use
+│ │ ├── logger
+│ │ ├── mcp-client
+│ │ ├── mcp-servers
+│ │ ├── search
+│ │ └── shared
+│ ├── common
+│ │ ├── configs
+│ │ └── electron-build
+│ └── ui-tars
+│ ├── action-parser
+│ ├── cli
+│ ├── electron-ipc
+│ ├── operators
+│ ├── sdk
+│ ├── shared
+│ ├── tsconfig.node.json
+│ ├── utio
+│ └── visualizer
+└── vitest.*.mts # Unit test configuration
+```
+
+> **Note**: The `src` directory is located in the top-level directory instead of the `apps/{main,preload,renderer}` directories because Electron Forge previously did not support Pnpm's hoisting mechanism([electron/forge#2633](https://github.com/electron/forge/issues/2633)), requiring the `src` directory to be placed in the top-level directory.
+
+
+#### Clone the repository
+
+```bash
+$ git clone https://github.com/bytedance/ui-tars-desktop.git
+$ cd ui-tars-desktop
+```
+
+### Development
+
+#### Install dependencies
+
+```bash
+$ pnpm install
+```
+
+#### Run the application
+
+```bash
+$ pnpm run dev:ui-tars # Start UI-TARS Desktop
+$ pnpm run dev:agent-tars # Start Agent-TARS Desktop
+```
+
+After the application starts, you can see the UI-TARS interface within the application.
+
+> **Note**: On MacOS, you need to grant permissions to the app (e.g., iTerm2, Terminal) you are using to run commands.
+
+#### Main process reload
+
+By default, `pnpm run dev` only has frontend Hot Module Replacement (HMR) hot updates. If you need to simultaneously reload the main process during debugging, you can execute `pnpm run dev:w`.
+
+```bash
+$ pnpm run dev:w
+```
+
+#### Building
+
+Run `pnpm run build` in current system, it will output into `out/*` directory.
+
+To build the products of other systems, run:
+- Mac x64: `pnpm run publish:mac-x64`
+- Mac ARM: `pnpm run publish:mac-arm64`
+- Windows x64: `pnpm run publish:win32`
+- Windows ARM: `pnpm run publish:win32-arm64`
+
+### Release
+
+#### Desktop Application
+
+The CI pipeline to execute is [.github/workflows/release.yml](.github/workflows/release.yml), only manual triggered by maintainers. If you're a maintainer, you can follow the steps below to release the application:
+
+1. Edit the `version` in `package.json`
+2. Git commit and push to the `release/${version}` branch, create a PR targeting `main` branch, titled `release(app): ${version}`
+3. Trigger the release [workflow](https://github.com/bytedance/UI-TARS-desktop/actions/workflows/release.yml) manually after the PR is merged
+
+Currently, the release workflow supports the following platforms:
+
+- MacOS x64
+- MacOS arm64
+- Windows x64
+
+#### Packages
+
+##### Latest version
+
+If you want to publish the `latest` version packages to the npm registry, you can run the following command:
+
+1. `pnpm changeset` to specify the changelogs for the packages you want to publish
+2. Git commit and push to the `release-pkgs/${version}` branch, create a PR targeting `main` branch, titled `release(pkgs): ${version}`
+3. `pnpm run publish:packages` to publish the packages in latest `origin/main` branch after the PR is merged
+
+##### Beta version
+
+If you want to publish the `beta` version packages to the npm registry, you can run the following command:
+
+1. `pnpm changeset` to specify the changelogs for the packages you want to publish
+2. Git commit and push to the branch
+3. `pnpm run publish-beta:packages` to publish the packages in current branch
+
+
+### Documentation
+
+The documents are placed in the `docs/*.md` directory, formatted in markdown. There is currently no documentation site, but the `docs/*.md` directory will be converted into a documentation site in the future.
+
+## Styleguides
+
+### Pre-commit Hooks
+
+We use [Husky](https://typicode.github.io/husky/#/) and [lint-staged](https://github.com/okonet/lint-staged) to enforce the pre-commit hooks. The hooks include:
+
+- `prettier --write` to format the code
+- `npm run typecheck` to strictly check the type
+
+### Commit Messages
+
+We use [Conventional Commits](https://www.conventionalcommits.org/) to standardize the commit messages.
+
+### CI / Testing
+
+Each PR or main branch push will trigger the CI pipeline to run the unit test and E2E test.
+
+#### Unit test
+
+```bash
+pnpm run test
+```
+
+#### E2E test
+
+```bash
+pnpm run test:e2e
+```
+
+## Submitting Changes
+
+* Push your changes to a feature branch in your fork of the repository.
+* Submit a pull request to this repository
+* Accept the CLA in your PR.
diff --git a/apps/agent-tars/README.md b/apps/agent-tars/README.md
new file mode 100644
index 000000000..9c7c5c1d9
--- /dev/null
+++ b/apps/agent-tars/README.md
@@ -0,0 +1,67 @@
+
+
+
+
+# Agent TARS
+
+
+
+
+
+
+**Agent TARS** is an open-source GUI agent designed to revolutionize multimodal interaction by visually interpreting web pages and seamlessly integrating with command lines and file systems.
+
+> [!CAUTION]
+> **DISCLAIMER**: Agent TARS is still in **Technical Preview** stage and not stable yet. It's not recommended to use it in production.
+
+## Showcases
+
+
+
+## ✨️ Features
+
+- **🌐 Advanced Browser Operations:** Executes sophisticated tasks like Deep Research and Operator functions through an agent framework, enabling comprehensive planning and execution.
+- **🛠️ Comprehensive Tool Support:** Integrates with search, file editing, command line, and Model Context Protocol (MCP) tools to handle complex workflows.
+- **💻️ Enhanced Desktop App:** A revamped UI with displays for browsers, multimodal elements, session management, model configuration, dialogue flow visualization, and browser/search status tracking.
+- **🔄 Workflow Orchestration:** Seamlessly connects GUI Agent tools—search, browse, explore links, and synthesize information into final outputs.
+- **⚙️ Developer-Friendly Framework:** Simplifies integration with UI-TARS and custom workflow creation for GUI Agent projects.
+
+## Getting Started
+
+See [Quick Start](./docs/quick-start.md).
+
+## Contributing
+
+Please read the [contributing guide](../../CONTRIBUTING.md) and let's build Agent TARS together.
+
+## Code of conduct
+
+This repo has adopted the ByteDance Open Source Code of Conduct. Please check [Code of conduct](../../CODE_OF_CONDUCT.md) for more details.
+
+## Roadmap
+
+**Agent TARS** is more than a tool —— it’s a platform for the future of multimodal agents. Upcoming enhancements include:
+
+- Ongoing optimization of agent framework —— GUI Agent synergy with expanded model compatibility.
+- Expansion to mobile device operations with cross-platform framework.
+- Integration with game environments for AI-driven gameplay.
+
+
+## Credits
+
+Thanks to:
+
+- The [browser-use](https://github.com/browser-use/browser-use) project whose work inspired us to better operate browsers
+- [@alexchenzl](https://github.com/alexchenzl) for developing the innovative [nanobrowser](https://github.com/nanobrowser/nanobrowser) Chrome extension, which provided valuable technical references during our browser control in Electron
+- [@EGOIST](https://github.com/egoist) for creating the remarkable AI chatbot [ChatWise](https://chatwise.app/), from which we drew significant inspiration for local browser detection and local browser search.
+- [Anthropic](https://www.anthropic.com/) for building the [Model Context Protocol](https://docs.anthropic.com/en/docs/agents-and-tools/mcp) to help us better manage local tools
+- [puppeteer](https://github.com/puppeteer/puppeteer) team for their excellent browser automation toolkit that greatly enhanced our workflow
+- [Web Infra](https://github.com/web-infra-dev) team and the [Rslib](https://github.com/web-infra-dev/rslib) project helps us build our libraries better.
+- The UI-TARS and UI-TARS-desktop development teams for laying crucial foundational frameworks
+- All contributors and members of the open-source community who supported this journey with their expertise and encouragement
+
+
+## License
+
+Agent TARS is [Apache License 2.0 licensed](https://github.com/bytedance/UI-TARS-desktop/blob/main/LICENSE).
+
diff --git a/apps/agent-tars/build/entitlements.mac.plist b/apps/agent-tars/build/entitlements.mac.plist
new file mode 100644
index 000000000..446fe171d
--- /dev/null
+++ b/apps/agent-tars/build/entitlements.mac.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+
+
diff --git a/apps/agent-tars/dev-app-update.yml b/apps/agent-tars/dev-app-update.yml
new file mode 100644
index 000000000..78fadeb7d
--- /dev/null
+++ b/apps/agent-tars/dev-app-update.yml
@@ -0,0 +1,3 @@
+provider: generic
+url: https://example.com/auto-updates
+updaterCacheDirName: open-agent-updater
diff --git a/apps/agent-tars/docs/quick-start.md b/apps/agent-tars/docs/quick-start.md
new file mode 100644
index 000000000..7e31a5393
--- /dev/null
+++ b/apps/agent-tars/docs/quick-start.md
@@ -0,0 +1,74 @@
+# Getting started with Agent TARS
+
+Hello, welcome to Agent TARS!
+
+This guide will walk you through the process of setting up your first Agent TARS project.
+
+## Necessary Configuration
+
+Before you begin, you will need to set some necessary configuration.
+
+You can click the left-bottom button to open the configuration page:
+
+
+
+Then you can set the model config and the search config.
+
+For model config, you can set the model provider and api key:
+
+
+
+> For Azure OpenAI, you can set more params, including apiVersion, deploymentName and endpoint.
+
+For search config, you can set the search provider and api key:
+
+
+
+## Start Your First Journey
+
+Now you can start your first journey in Agent TARS!
+
+You can input your first question in the input box, and then press Enter to send your question. Here is an example:
+
+
+
+It's working!
+
+We also support **Human In the Loop**, that means you can interact with the agent in the working process by the input box. If you want to change the direction of current agent work, you can insert your thoughts in the special input box on the top position, and then press Enter to send your thoughts. Here is an example:
+
+
+
+## Share Your Thead
+
+You can share your thread with others by the share button on the top menu.
+
+There are two modes to share your thread:
+
+- **Local Html**: Agent TARS will bundle your thread into a html file, and you can share it with others.
+- **Remote Server Url**: Agent TARS will generate a url for you to share your thread with others, Agent TARS will upload the html bundle to a remote server.
+
+### Local Mode
+
+You can click the share button to open the share modal, and then click the **Local Html** button to share your thread.
+
+
+
+### Remote Mode
+
+For the remote share mode, you need to set the remote server url in the share modal:
+
+
+
+Then Agent TARS will post a request to the remote server to upload the html bundle, and then you can share the url with others. The specific request information is as follows:
+
+- Method: POST
+- Body:
+ - file: the html bundle file(type: multipart/form-data)
+- Response:
+ - data: { url: string }
+
+Then the server will return an object including the `url` parameter, which is the url to share your thread.
+
+### Enjoy the replay experience!
+
+When you finish the shared process, you can preview the bundle and experience the wonderful replay process! That's really cool!
diff --git a/apps/agent-tars/electron-builder.yml b/apps/agent-tars/electron-builder.yml
new file mode 100644
index 000000000..7a54efa8f
--- /dev/null
+++ b/apps/agent-tars/electron-builder.yml
@@ -0,0 +1,45 @@
+appId: com.electron.app
+productName: open-agent
+directories:
+ buildResources: build
+files:
+ - '!**/.vscode/*'
+ - '!src/*'
+ - '!electron.vite.config.{js,ts,mjs,cjs}'
+ - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
+ - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
+ - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
+asarUnpack:
+ - resources/**
+win:
+ executableName: open-agent
+nsis:
+ artifactName: ${name}-${version}-setup.${ext}
+ shortcutName: ${productName}
+ uninstallDisplayName: ${productName}
+ createDesktopShortcut: always
+mac:
+ entitlementsInherit: build/entitlements.mac.plist
+ extendInfo:
+ - NSCameraUsageDescription: Application requests access to the device's camera.
+ - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
+ - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
+ - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
+ notarize: false
+dmg:
+ artifactName: ${name}-${version}.${ext}
+linux:
+ target:
+ - AppImage
+ - snap
+ - deb
+ maintainer: electronjs.org
+ category: Utility
+appImage:
+ artifactName: ${name}-${version}.${ext}
+npmRebuild: false
+publish:
+ provider: generic
+ url: https://example.com/auto-updates
+electronDownload:
+ mirror: https://npmmirror.com/mirrors/electron/
diff --git a/apps/agent-tars/electron.vite.config.ts b/apps/agent-tars/electron.vite.config.ts
new file mode 100644
index 000000000..c999c14f1
--- /dev/null
+++ b/apps/agent-tars/electron.vite.config.ts
@@ -0,0 +1,63 @@
+import { resolve } from 'node:path';
+import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
+import react from '@vitejs/plugin-react';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+export default defineConfig({
+ main: {
+ root: 'src/main',
+ build: {
+ outDir: resolve(__dirname, './dist/main'),
+ },
+ plugins: [externalizeDepsPlugin(), tsconfigPaths()],
+ },
+ preload: {
+ root: 'src/preload',
+ build: {
+ outDir: resolve(__dirname, './dist/preload'),
+ },
+ plugins: [externalizeDepsPlugin(), tsconfigPaths()],
+ },
+ renderer: {
+ optimizeDeps: {
+ include: [
+ '@monaco-editor/react',
+ 'monaco-editor/esm/vs/language/json/json.worker',
+ 'monaco-editor/esm/vs/language/css/css.worker',
+ 'monaco-editor/esm/vs/language/html/html.worker',
+ 'monaco-editor/esm/vs/language/typescript/ts.worker',
+ 'monaco-editor/esm/vs/editor/editor.worker',
+ ],
+ },
+ define: {
+ 'process.env.REPORT_HTML_MODE': 'false',
+ },
+ worker: {
+ format: 'es',
+ },
+ root: 'src/renderer',
+ build: {
+ outDir: resolve(__dirname, './dist/renderer'),
+ rollupOptions: {
+ input: {
+ main: resolve('./src/renderer/index.html'),
+ },
+ output: {
+ manualChunks: {
+ monaco: ['monaco-editor'],
+ },
+ },
+ },
+ minify: true,
+ },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ api: 'modern',
+ },
+ },
+ },
+
+ plugins: [react(), tsconfigPaths()],
+ },
+});
diff --git a/apps/agent-tars/forge.config.ts b/apps/agent-tars/forge.config.ts
new file mode 100644
index 000000000..bb0ee7e16
--- /dev/null
+++ b/apps/agent-tars/forge.config.ts
@@ -0,0 +1,231 @@
+import fs, { readdirSync } from 'node:fs';
+import { cp, readdir } from 'node:fs/promises';
+import path, { resolve } from 'node:path';
+import { MakerDMG } from '@electron-forge/maker-dmg';
+import { MakerZIP } from '@electron-forge/maker-zip';
+import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
+import { FusesPlugin } from '@electron-forge/plugin-fuses';
+import type { ForgeConfig } from '@electron-forge/shared-types';
+import { FuseV1Options, FuseVersion } from '@electron/fuses';
+import setLanguages from 'electron-packager-languages';
+import { rimraf, rimrafSync } from 'rimraf';
+import {
+ getModuleRoot,
+ getExternalPkgsDependencies,
+} from '@common/electron-build';
+
+import pkg from './package.json';
+
+const projectRoot = path.resolve(__dirname, '.');
+
+const keepModules = new Set([
+ ...Object.keys(pkg.dependencies),
+ '@mixmark-io/domino',
+ '@modelcontextprotocol/sdk',
+]);
+const needSubDependencies = ['@modelcontextprotocol/sdk'];
+const keepLanguages = new Set(['en', 'en_GB', 'en-US', 'en_US']);
+const ignorePattern = new RegExp(
+ `^/node_modules/(?!${[...keepModules].join('|')})`,
+);
+
+console.log('keepModules', keepModules);
+console.log('needSubDependencies', needSubDependencies);
+console.log('ignorePattern', ignorePattern);
+
+const enableOsxSign =
+ process.env.APPLE_ID &&
+ process.env.APPLE_PASSWORD &&
+ process.env.APPLE_TEAM_ID;
+
+// remove folders & files not to be included in the app
+async function cleanSources(
+ buildPath,
+ _electronVersion,
+ platform,
+ _arch,
+ callback,
+) {
+ // folders & files to be included in the app
+ const appItems = new Set([
+ 'dist',
+ 'node_modules',
+ 'package.json',
+ 'resources',
+ ]);
+
+ console.log('buildPath', buildPath);
+
+ if (platform === 'darwin') {
+ const frameworkResourcePath = resolve(
+ buildPath,
+ '../../Frameworks/Electron Framework.framework/Versions/A/Resources',
+ );
+
+ for (const file of readdirSync(frameworkResourcePath)) {
+ if (file.endsWith('.lproj') && !keepLanguages.has(file.split('.')[0])) {
+ rimrafSync(resolve(frameworkResourcePath, file));
+ }
+ }
+ }
+
+ // Keep only node_modules to be included in the app
+ await Promise.all([
+ ...(await readdir(buildPath).then((items) =>
+ items
+ .filter((item) => !appItems.has(item))
+ .map((item) => rimraf(path.join(buildPath, item))),
+ )),
+ ...(await readdir(path.join(buildPath, 'node_modules')).then((items) =>
+ items
+ .filter((item) => !keepModules.has(item))
+ .map((item) => rimraf(path.join(buildPath, 'node_modules', item))),
+ )),
+ ]);
+
+ await Promise.all(
+ Array.from(keepModules.values()).map((item) => {
+ // Check is exist
+ if (fs.existsSync(path.join(buildPath, 'node_modules', item))) {
+ // eslint-disable-next-line array-callback-return
+ return;
+ }
+
+ try {
+ const moduleRoot = getModuleRoot(projectRoot, item);
+
+ if (fs.existsSync(moduleRoot)) {
+ console.log('copy_current_node_modules', moduleRoot);
+ return cp(moduleRoot, path.join(buildPath, 'node_modules', item), {
+ recursive: true,
+ });
+ }
+ } catch (error) {
+ console.error('copy_current_node_modules', error);
+ return;
+ }
+
+ return;
+ }),
+ );
+
+ const subDependencies = await getExternalPkgsDependencies(
+ needSubDependencies,
+ projectRoot,
+ );
+ console.log('subDependencies', subDependencies);
+ await Promise.all(
+ subDependencies.map((subDependency) => {
+ console.log(
+ 'target_dir',
+ path.join(buildPath, 'node_modules', subDependency.name),
+ );
+ const targetDir = path.join(
+ buildPath,
+ 'node_modules',
+ subDependency.name,
+ );
+ const sourceDir = subDependency.path;
+ if (!fs.existsSync(targetDir) && fs.existsSync(sourceDir)) {
+ console.log('copy_current_node_modules', sourceDir);
+ return cp(sourceDir, targetDir, {
+ recursive: true,
+ });
+ }
+ return;
+ }),
+ );
+
+ callback();
+}
+
+const noopAfterCopy = (
+ _buildPath,
+ _electronVersion,
+ _platform,
+ _arch,
+ callback,
+) => callback();
+
+const config: ForgeConfig = {
+ packagerConfig: {
+ // appBundleId: 'com.bytedance.uitars',
+ name: 'Agent TARS',
+ icon: 'resources/icon',
+ asar: true,
+ ignore: [ignorePattern],
+ prune: false,
+ afterCopy: [
+ cleanSources,
+ process.platform !== 'win32'
+ ? noopAfterCopy
+ : setLanguages(Array.from(keepLanguages)),
+ ],
+ executableName: 'Agent-TARS',
+ ...(enableOsxSign
+ ? {
+ osxSign: {
+ keychain: process.env.KEYCHAIN_PATH,
+ optionsForFile: () => ({
+ entitlements: 'build/entitlements.mac.plist',
+ }),
+ },
+ osxNotarize: {
+ appleId: process.env.APPLE_ID!,
+ appleIdPassword: process.env.APPLE_PASSWORD!,
+ teamId: process.env.APPLE_TEAM_ID!,
+ },
+ }
+ : {}),
+ },
+ rebuildConfig: {},
+ publishers: [
+ {
+ name: '@electron-forge/publisher-github',
+ config: {
+ repository: { owner: 'bytedance', name: 'ui-tars-desktop' },
+ draft: true,
+ force: true,
+ generateReleaseNotes: true,
+ tagPrefix: 'Agent-TARS-v',
+ },
+ },
+ ],
+ makers: [
+ new MakerZIP({}, ['darwin']),
+ // https://github.com/electron/forge/issues/3712
+ new MakerDMG({
+ overwrite: true,
+ background: 'static/dmg-background.png',
+ // icon: 'static/dmg-icon.icns',
+ iconSize: 160,
+ format: 'UDZO',
+ additionalDMGOptions: { window: { size: { width: 660, height: 400 } } },
+ contents: (opts) => [
+ { x: 180, y: 170, type: 'file', path: opts.appPath },
+ { x: 480, y: 170, type: 'link', path: '/Applications' },
+ ],
+ }),
+ ],
+ plugins: [
+ new AutoUnpackNativesPlugin({}),
+ // Fuses are used to enable/disable various Electron functionality
+ // at package time, before code signing the application
+ // https://github.com/microsoft/playwright/issues/28669#issuecomment-2268380066
+ ...(process.env.CI === 'e2e'
+ ? []
+ : [
+ new FusesPlugin({
+ version: FuseVersion.V1,
+ [FuseV1Options.RunAsNode]: false,
+ [FuseV1Options.EnableCookieEncryption]: true,
+ [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
+ [FuseV1Options.EnableNodeCliInspectArguments]: false,
+ [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
+ [FuseV1Options.OnlyLoadAppFromAsar]: true,
+ }),
+ ]),
+ ],
+};
+
+export default config;
diff --git a/apps/agent-tars/package.json b/apps/agent-tars/package.json
new file mode 100644
index 000000000..6d22e3c55
--- /dev/null
+++ b/apps/agent-tars/package.json
@@ -0,0 +1,93 @@
+{
+ "name": "agent-tars-app",
+ "version": "1.0.0-alpha.1",
+ "description": "A multimodal AI agent that revolutionizes GUI interaction",
+ "main": "./dist/main/index.js",
+ "author": "ByteDance",
+ "homepage": "https://electron-vite.org",
+ "packageManager": "pnpm@9.12.3",
+ "scripts": {
+ "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
+ "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
+ "typecheck": "npm run typecheck:node && npm run typecheck:web",
+ "asar:analyze": "asar extract out/Agent\\ TARS-darwin-arm64/Agent\\ TARS.app/Contents/Resources/app.asar ./dist/asar",
+ "start": "electron-vite preview",
+ "dev": "electron-vite dev",
+ "prepare": "npm run build:reporter",
+ "package": "electron-forge package",
+ "build": "rimraf dist out && npm run typecheck && npm run build:reporter && electron-vite build && electron-forge make",
+ "test": "vitest run",
+ "publish:mac": "npm run build:reporter && electron-vite build && electron-forge publish --arch=universal --platform=darwin",
+ "dev:reporter": "vite dev",
+ "build:reporter": "vite build"
+ },
+ "peerDependencies": {
+ "esbuild-register": "*",
+ "ts-node": "*",
+ "tsx": "*"
+ },
+ "peerDependenciesMeta": {
+ "esbuild-register": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ }
+ },
+ "dependencies": {
+ "@agent-infra/mcp-server-commands": "workspace:*",
+ "@agent-infra/mcp-server-filesystem": "workspace:*",
+ "@agent-infra/mcp-server-browser": "workspace:*"
+ },
+ "devDependencies": {
+ "@modelcontextprotocol/sdk": "^1.7.0",
+ "@electron-toolkit/preload": "^3.0.1",
+ "@electron-toolkit/utils": "^4.0.0",
+ "@electron/asar": "^3.2.18",
+ "update-electron-app": "^3.1.0",
+ "openai": "^4.86.2",
+ "dotenv": "16.4.7",
+ "@agent-infra/shared": "workspace:*",
+ "@agent-infra/mcp-client": "workspace:*",
+ "@agent-infra/search": "workspace:*",
+ "ws": "8.18.1",
+ "utf-8-validate": "6.0.5",
+ "bufferutil": "4.0.9",
+ "fs-extra": "11.3.0",
+ "@anthropic-ai/sdk": "0.39.0",
+ "@google/generative-ai": "0.24.0",
+ "@mistralai/mistralai": "1.5.1",
+ "@ui-tars/electron-ipc": "workspace:*",
+ "@common/electron-build": "workspace:*",
+ "@electron-forge/cli": "^7.7.0",
+ "@electron-forge/maker-deb": "^7.7.0",
+ "@electron-forge/maker-dmg": "^7.7.0",
+ "@electron-forge/maker-rpm": "^7.7.0",
+ "@electron-forge/maker-squirrel": "^7.7.0",
+ "@electron-forge/maker-zip": "^7.7.0",
+ "@electron-forge/plugin-auto-unpack-natives": "^7.7.0",
+ "@electron-forge/plugin-fuses": "^7.7.0",
+ "@electron-forge/plugin-vite": "^7.7.0",
+ "@electron-forge/publisher-github": "^7.7.0",
+ "@electron-toolkit/tsconfig": "^1.0.1",
+ "@types/node": "^22.13.4",
+ "@types/fs-extra": "11.0.4",
+ "@vitejs/plugin-react": "^4.3.4",
+ "rimraf": "^6.0.1",
+ "autoprefixer": "10.4.20",
+ "electron": "34.1.1",
+ "electron-packager-languages": "0.6.0",
+ "electron-vite": "^3.0.0",
+ "jsdom": "^26.0.0",
+ "sass": "1.85.1",
+ "tailwindcss": "^3.3.3",
+ "typescript": "^5.7.3",
+ "vite": "^6.1.0",
+ "vite-tsconfig-paths": "^5.1.4",
+ "vitest": "^3.0.8",
+ "vite-plugin-singlefile": "2.2.0"
+ }
+}
diff --git a/apps/agent-tars/postcss.config.js b/apps/agent-tars/postcss.config.js
new file mode 100644
index 000000000..12a703d90
--- /dev/null
+++ b/apps/agent-tars/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/apps/agent-tars/resources/icon.icns b/apps/agent-tars/resources/icon.icns
new file mode 100644
index 000000000..e103b6f13
Binary files /dev/null and b/apps/agent-tars/resources/icon.icns differ
diff --git a/apps/agent-tars/resources/icon.ico b/apps/agent-tars/resources/icon.ico
new file mode 100644
index 000000000..ddd728e41
Binary files /dev/null and b/apps/agent-tars/resources/icon.ico differ
diff --git a/apps/agent-tars/resources/icon.png b/apps/agent-tars/resources/icon.png
new file mode 100644
index 000000000..6e9a3a185
Binary files /dev/null and b/apps/agent-tars/resources/icon.png differ
diff --git a/apps/agent-tars/src/main/customTools/index.ts b/apps/agent-tars/src/main/customTools/index.ts
new file mode 100644
index 000000000..a257ed7ae
--- /dev/null
+++ b/apps/agent-tars/src/main/customTools/index.ts
@@ -0,0 +1,32 @@
+import { ToolCall } from '@agent-infra/shared';
+import { search } from './search';
+
+export function executeCustomTool(toolCall: ToolCall) {
+ if (toolCall.function.name === 'web_search') {
+ return search(toolCall);
+ }
+
+ return null;
+}
+
+export function listCustomTools() {
+ return [
+ {
+ type: 'function',
+ function: {
+ name: 'web_search',
+ description: 'Search in the internet',
+ parameters: {
+ type: 'object',
+ properties: {
+ query: {
+ type: 'string',
+ description: 'The search query',
+ },
+ },
+ required: ['query'],
+ },
+ },
+ },
+ ] as const;
+}
diff --git a/apps/agent-tars/src/main/customTools/search.ts b/apps/agent-tars/src/main/customTools/search.ts
new file mode 100644
index 000000000..ebf1f515e
--- /dev/null
+++ b/apps/agent-tars/src/main/customTools/search.ts
@@ -0,0 +1,46 @@
+import { SearchSettings, ToolCall } from '@agent-infra/shared';
+import { SearchClient, SearchProvider } from '@agent-infra/search';
+import { MCPToolResult } from '@main/type';
+
+let currentSearchConfig: SearchSettings | null = null;
+
+export function updateSearchConfig(config: SearchSettings) {
+ currentSearchConfig = config;
+}
+
+export async function search(toolCall: ToolCall): Promise {
+ if (!currentSearchConfig) {
+ throw new Error('Search configuration not set');
+ }
+
+ const args = JSON.parse(toolCall.function.arguments);
+
+ const client = new SearchClient({
+ provider: SearchProvider.BingSearch,
+ providerConfig: {
+ apiKey: currentSearchConfig.apiKey,
+ baseUrl: currentSearchConfig.baseUrl,
+ },
+ });
+
+ try {
+ const results = await client.search({
+ query: args.query,
+ count: 10,
+ });
+
+ return [
+ {
+ isError: false,
+ content: results,
+ },
+ ];
+ } catch (e) {
+ return [
+ {
+ isError: true,
+ content: [JSON.stringify(e)],
+ },
+ ];
+ }
+}
diff --git a/apps/agent-tars/src/main/index.ts b/apps/agent-tars/src/main/index.ts
new file mode 100644
index 000000000..c5ca3c715
--- /dev/null
+++ b/apps/agent-tars/src/main/index.ts
@@ -0,0 +1,103 @@
+import { app, shell, BrowserWindow, ipcMain } from 'electron';
+import { join } from 'path';
+import { registerIpcMain } from '@ui-tars/electron-ipc/main';
+import { electronApp, optimizer, is } from '@electron-toolkit/utils';
+import { ipcRoutes } from './ipcRoutes';
+import icon from '../../resources/icon.png?asset';
+
+function createWindow(): void {
+ // Create the browser window.
+ const mainWindow = new BrowserWindow({
+ width: 900,
+ height: 670,
+ show: false,
+ autoHideMenuBar: true,
+ frame: false,
+ titleBarStyle: 'hidden',
+ trafficLightPosition: { x: 10, y: 10 },
+ ...(process.platform === 'linux' ? { icon } : {}),
+ webPreferences: {
+ preload: join(__dirname, '../preload/index.js'),
+ sandbox: false,
+ // Allow local file access
+ webSecurity: false,
+ allowRunningInsecureContent: false,
+ contextIsolation: true,
+ },
+ });
+
+ mainWindow.webContents.session.webRequest.onHeadersReceived(
+ (details, callback) => {
+ callback({
+ responseHeaders: {
+ ...details.responseHeaders,
+ 'Content-Security-Policy': [
+ "default-src 'self' 'unsafe-inline' file: data: blob:;",
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdn.jsdelivr.net/npm/ https://cdnjs.cloudflare.com;",
+ "img-src 'self' file: data: blob:;",
+ "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com;",
+ "font-src 'self' https://cdnjs.cloudflare.com https://fonts.googleapis.com;",
+ "worker-src 'self' blob:;",
+ "connect-src 'self' https://cdn.jsdelivr.net/npm/ blob:",
+ ],
+ },
+ });
+ },
+ );
+
+ mainWindow.on('ready-to-show', () => {
+ mainWindow.show();
+ });
+
+ mainWindow.webContents.setWindowOpenHandler((details) => {
+ shell.openExternal(details.url);
+ return { action: 'deny' };
+ });
+
+ // HMR for renderer base on electron-vite cli.
+ // Load the remote URL for development or the local html file for production.
+ if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
+ mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
+ } else {
+ mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
+ }
+}
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+// Some APIs can only be used after this event occurs.
+app.whenReady().then(() => {
+ // Set app user model id for windows
+ electronApp.setAppUserModelId('com.electron');
+
+ // Default open or close DevTools by F12 in development
+ // and ignore CommandOrControl + R in production.
+ // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
+ app.on('browser-window-created', (_, window) => {
+ optimizer.watchWindowShortcuts(window);
+ });
+
+ // IPC test
+ ipcMain.on('ping', () => console.log('pong'));
+ registerIpcMain(ipcRoutes);
+
+ createWindow();
+
+ app.on('activate', function () {
+ // On macOS it's common to re-create a window in the app when the
+ // dock icon is clicked and there are no other windows open.
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
+ });
+});
+
+// Quit when all windows are closed, except on macOS. There, it's common
+// for applications and their menu bar to stay active until the user quits
+// explicitly with Cmd + Q.
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit();
+ }
+});
+
+// In this file you can include the rest of your app's specific main process
+// code. You can also put them in separate files and require them here.
diff --git a/apps/agent-tars/src/main/ipcRoutes/action.ts b/apps/agent-tars/src/main/ipcRoutes/action.ts
new file mode 100644
index 000000000..0724973b6
--- /dev/null
+++ b/apps/agent-tars/src/main/ipcRoutes/action.ts
@@ -0,0 +1,215 @@
+import { MCPServerName, ToolCall } from '@agent-infra/shared';
+import { executeCustomTool, listCustomTools } from '@main/customTools';
+import { createMcpClient, getOmegaDir } from '@main/mcp/client';
+import { mcpToolsToAzureTools } from '@main/mcp/tools';
+import { MCPToolResult } from '@main/type';
+import { initIpc } from '@ui-tars/electron-ipc/main';
+import { ChatCompletionTool } from 'openai/resources/index.mjs';
+import path from 'path';
+import fs, { readFile } from 'fs-extra';
+import { shell } from 'electron';
+import fetch from 'node-fetch';
+import FormData from 'form-data';
+import {
+ normalizeMessages,
+ parseArtifacts,
+} from '@main/utils/normalizeOmegaData';
+
+export interface MCPTool {
+ id: string;
+ serverName: string;
+ name?: string;
+ description?: string;
+ inputSchema?: Record;
+}
+
+const t = initIpc.create();
+
+/**
+ * Convert tool use data from OpenAI to MCP tool format
+ */
+function toolUseToMcpTool(
+ mcpTools: MCPTool[] | undefined,
+ toolUse: ToolCall,
+): MCPTool | undefined {
+ if (!mcpTools) return undefined;
+ const tool = mcpTools.find((tool) => tool.name === toolUse.function.name);
+ if (!tool) return undefined;
+ tool.inputSchema = JSON.parse(toolUse.function.arguments);
+ return tool;
+}
+
+export const actionRoute = t.router({
+ listTools: t.procedure.handle(async () => {
+ const mcpClient = await createMcpClient();
+ const tools = mcpToolsToAzureTools(await mcpClient.listTools());
+ const customTools = listCustomTools();
+ return [
+ ...tools.map((tool) => tool.function),
+ ...customTools.map((tool) => tool.function),
+ ] as ChatCompletionTool['function'][];
+ }),
+
+ listMcpTools: t.procedure.handle(async () => {
+ const mcpClient = await createMcpClient();
+ const tools = await mcpClient.listTools();
+ return tools;
+ }),
+
+ listCustomTools: t.procedure.handle(async () => {
+ const customTools = listCustomTools();
+ return customTools;
+ }),
+
+ executeTool: t.procedure
+ .input<{
+ toolCalls: ToolCall[];
+ }>()
+ .handle(async ({ input }) => {
+ const mcpClient = await createMcpClient();
+ const tools = await mcpClient.listTools();
+ const results: MCPToolResult = [];
+ for (const toolCall of input.toolCalls) {
+ const mcpTool = toolUseToMcpTool(tools, toolCall);
+ if (mcpTool) {
+ console.log('i will execute tool', mcpTool.name, mcpTool.inputSchema);
+ try {
+ const result = await mcpClient.callTool({
+ client: mcpTool.serverName as MCPServerName,
+ name: mcpTool.name as string,
+ args: mcpTool.inputSchema || {},
+ });
+ results.push(result);
+ } catch (e) {
+ console.error('execute tool error', mcpTool, e);
+ results.push({
+ isError: true,
+ content: [JSON.stringify(e)],
+ });
+ }
+ } else {
+ const result = await executeCustomTool(toolCall);
+ if (result) {
+ results.push(...result);
+ }
+ }
+ }
+ return results;
+ }),
+
+ saveBrowserSnapshot: t.procedure.input().handle(async () => {
+ const mcpClient = await createMcpClient();
+ try {
+ const result = await mcpClient.callTool({
+ client: MCPServerName.Browser,
+ name: 'browser_screenshot',
+ args: {},
+ });
+ const screenshotMeta = (
+ result.content as [
+ { type: 'text'; text: string },
+ { type: 'image'; data: string; mimeType: string },
+ ]
+ )[1];
+ const omegaDir = await getOmegaDir();
+ const screenshotPath = path.join(omegaDir, 'screenshots');
+ await fs.mkdirSync(screenshotPath, { recursive: true });
+
+ const ext = screenshotMeta.mimeType.split('/')[1] || 'png';
+ const timestamp = new Date().getTime();
+ const filename = `screenshot_${timestamp}.${ext}`;
+ const filepath = path.join(screenshotPath, filename);
+
+ const imageBuffer = Buffer.from(screenshotMeta.data, 'base64');
+ await fs.writeFile(filepath, imageBuffer);
+ return { filepath };
+ } catch (e) {
+ console.error('Failed to save screenshot:', e);
+ throw e;
+ }
+ }),
+
+ saveReportHtml: t.procedure
+ .input<{ messages: any; reportApiUrl?: string }>()
+ .handle(async ({ input }) => {
+ const { messages: rawMessages, reportApiUrl } = input;
+ const messages = await normalizeMessages(rawMessages);
+ const omegaDir = await getOmegaDir();
+ const reportHtmlTemplate = await readFile(
+ path.join(__dirname, '../reporter/index.html'),
+ 'utf-8',
+ );
+ const artifacts = await parseArtifacts(messages);
+ const reportContent = reportHtmlTemplate
+ .replace(
+ ' ',
+ '',
+ )
+ .replace(
+ /.*?<\/title>/,
+ `${messages?.[0]?.content || 'Agent TARS'} `,
+ );
+
+ if (reportApiUrl) {
+ const tempPath = path.join(
+ omegaDir,
+ `temp_report_${new Date().getTime()}.html`,
+ );
+ await fs.writeFile(tempPath, reportContent);
+
+ try {
+ const formData = new FormData();
+ const fileBuffer = await fs.readFile(tempPath);
+ formData.append('file', fileBuffer, {
+ filename: 'report.html',
+ contentType: 'text/html',
+ });
+
+ const res = await fetch(reportApiUrl, {
+ method: 'POST',
+ body: formData,
+ headers: formData.getHeaders(),
+ });
+
+ await fs.remove(tempPath);
+
+ if (!res.ok) {
+ console.error('Upload failed:', await res.text());
+ throw new Error('文件上传失败');
+ }
+
+ const data = await res.json();
+ if (!data.url) {
+ throw new Error('文件上传失败:服务器未返回 url');
+ }
+
+ await shell.openExternal(data.url);
+ return data.url;
+ } catch (error) {
+ console.error('Upload failed:', error);
+ throw error;
+ }
+ } else {
+ const reportPath = path.join(
+ omegaDir,
+ `report_${new Date().getTime()}.html`,
+ );
+ await fs.writeFile(reportPath, reportContent);
+
+ // Open the report in the default browser
+ await shell.openPath(reportPath);
+ return reportPath;
+ }
+ }),
+
+ cleanup: t.procedure.handle(async () => {
+ const mcpClient = await createMcpClient();
+ await mcpClient.cleanup();
+ return true;
+ }),
+});
diff --git a/apps/agent-tars/src/main/ipcRoutes/agent.ts b/apps/agent-tars/src/main/ipcRoutes/agent.ts
new file mode 100644
index 000000000..30e24b9d8
--- /dev/null
+++ b/apps/agent-tars/src/main/ipcRoutes/agent.ts
@@ -0,0 +1,10 @@
+import { initIpc } from '@ui-tars/electron-ipc/main';
+
+const t = initIpc.create();
+
+export const agentRoute = t.router({
+ runAgent: t.procedure.input().handle(async () => {
+ console.log('runAgent');
+ return 'Hello';
+ }),
+});
diff --git a/apps/agent-tars/src/main/ipcRoutes/browser.ts b/apps/agent-tars/src/main/ipcRoutes/browser.ts
new file mode 100644
index 000000000..20eeb5fea
--- /dev/null
+++ b/apps/agent-tars/src/main/ipcRoutes/browser.ts
@@ -0,0 +1,9 @@
+import { initIpc } from '@ui-tars/electron-ipc/main';
+
+const t = initIpc.create();
+
+export const browserRoute = t.router({
+ // getScreenshot: t.procedure.input().handle(async (key: string) => {
+ // return getScreenshots(key);
+ // }),
+});
diff --git a/apps/agent-tars/src/main/ipcRoutes/filesystem.ts b/apps/agent-tars/src/main/ipcRoutes/filesystem.ts
new file mode 100644
index 000000000..424710b1c
--- /dev/null
+++ b/apps/agent-tars/src/main/ipcRoutes/filesystem.ts
@@ -0,0 +1,56 @@
+import { initIpc } from '@ui-tars/electron-ipc/main';
+import { setAllowedDirectories, getAllowedDirectories } from '@main/mcp/client';
+import path from 'path';
+import os from 'os';
+import fs from 'fs-extra';
+
+const t = initIpc.create();
+
+interface FileSystemSettings {
+ availableDirectories: string[];
+}
+
+export const fileSystemRoute = t.router({
+ updateFileSystemConfig: t.procedure
+ .input()
+ .handle(async ({ input }) => {
+ try {
+ // Always include the default .omega directory
+ const omegaDir = path.join(os.homedir(), '.omega');
+
+ // Combine with user-specified directories, ensuring no duplicates
+ const allDirectories = [
+ omegaDir,
+ ...input.availableDirectories.filter((dir) => dir !== omegaDir),
+ ];
+
+ // Update allowed directories in the MCP file system client
+ await setAllowedDirectories(allDirectories);
+
+ return true;
+ } catch (error) {
+ return false;
+ }
+ }),
+
+ getFileContent: t.procedure
+ .input<{ filePath: string }>()
+ .handle(async ({ input }) => {
+ try {
+ const content = await fs.readFile(input.filePath, 'utf8');
+ return content;
+ } catch (error) {
+ console.error('Failed to read file:', error);
+ return null;
+ }
+ }),
+ getAllowedDirectories: t.procedure.input().handle(async () => {
+ try {
+ return await getAllowedDirectories();
+ } catch (error) {
+ console.error('Failed to get allowed directories:', error);
+ const omegaDir = path.join(os.homedir(), '.omega');
+ return [omegaDir];
+ }
+ }),
+});
diff --git a/apps/agent-tars/src/main/ipcRoutes/index.ts b/apps/agent-tars/src/main/ipcRoutes/index.ts
new file mode 100644
index 000000000..87ea18a34
--- /dev/null
+++ b/apps/agent-tars/src/main/ipcRoutes/index.ts
@@ -0,0 +1,21 @@
+import { initIpc, createServer } from '@ui-tars/electron-ipc/main';
+import { agentRoute } from './agent';
+import { llmRoute } from './llm';
+import { actionRoute } from './action';
+import { browserRoute } from './browser';
+import { fileSystemRoute } from './filesystem';
+import { searchRoute } from './search';
+
+const t = initIpc.create();
+
+export const ipcRoutes = t.router({
+ ...agentRoute,
+ ...llmRoute,
+ ...actionRoute,
+ ...browserRoute,
+ ...fileSystemRoute,
+ ...searchRoute,
+});
+export type Router = typeof ipcRoutes;
+
+export const server = createServer(ipcRoutes);
diff --git a/apps/agent-tars/src/main/ipcRoutes/llm.ts b/apps/agent-tars/src/main/ipcRoutes/llm.ts
new file mode 100644
index 000000000..8f88c5479
--- /dev/null
+++ b/apps/agent-tars/src/main/ipcRoutes/llm.ts
@@ -0,0 +1,124 @@
+import { MCPServerName, Message, MessageData } from '@agent-infra/shared';
+import { initIpc } from '@ui-tars/electron-ipc/main';
+import { ChatCompletionTool } from 'openai/resources/index.mjs';
+import { BrowserWindow } from 'electron';
+import { createLLM, LLMConfig } from '@main/llmProvider';
+import { ProviderFactory } from '@main/llmProvider/ProviderFactory';
+
+const t = initIpc.create();
+
+const currentLLMConfigRef: {
+ current: LLMConfig;
+} = {
+ current: {},
+};
+
+export const llmRoute = t.router({
+ askLLMText: t.procedure
+ .input<{
+ messages: MessageData[];
+ systemMsgs?: MessageData[];
+ requestId: string;
+ }>()
+ .handle(async ({ input }) => {
+ const messages = input.messages.map((msg) => new Message(msg));
+ const llm = createLLM(currentLLMConfigRef.current);
+ const response = await llm.askLLMText({
+ messages,
+ requestId: input.requestId,
+ });
+ return response;
+ }),
+
+ askLLMTool: t.procedure
+ .input<{
+ messages: MessageData[];
+ tools: ChatCompletionTool[];
+ mcpServerKeys?: MCPServerName[];
+ requestId: string;
+ }>()
+ .handle(async ({ input }) => {
+ const messages = input.messages.map((msg) => new Message(msg));
+ const llm = createLLM(currentLLMConfigRef.current);
+ console.log('current llm config', currentLLMConfigRef.current);
+ const response = await llm.askTool({
+ messages,
+ tools: input.tools,
+ mcpServerKeys: input.mcpServerKeys,
+ requestId: input.requestId,
+ });
+ return response;
+ }),
+
+ askLLMTextStream: t.procedure
+ .input<{
+ messages: MessageData[];
+ systemMsgs?: MessageData[];
+ requestId: string;
+ }>()
+ .handle(async ({ input }) => {
+ const messages = input.messages.map((msg) => new Message(msg));
+ const { requestId } = input;
+ console.log('current llm config', currentLLMConfigRef.current);
+ const llm = createLLM(currentLLMConfigRef.current);
+
+ (async () => {
+ const windows = BrowserWindow.getAllWindows();
+ try {
+ const stream = llm.askLLMTextStream({ messages, requestId });
+
+ for await (const chunk of stream) {
+ if (!windows.length) {
+ return;
+ }
+
+ windows.forEach((win) => {
+ win.webContents.send(`llm:stream:${requestId}:data`, chunk);
+ });
+ }
+
+ windows.forEach((win) => {
+ win.webContents.send(`llm:stream:${requestId}:end`);
+ });
+ } catch (error) {
+ windows.forEach((win) => {
+ win.webContents.send(`llm:stream:${requestId}:error`, error);
+ });
+ }
+ })();
+
+ return requestId;
+ }),
+
+ updateLLMConfig: t.procedure.input().handle(async ({ input }) => {
+ try {
+ console.log('input entered', input);
+ currentLLMConfigRef.current = input;
+ return true;
+ } catch (error) {
+ console.error('Failed to update LLM configuration:', error);
+ return false;
+ }
+ }),
+
+ getAvailableProviders: t.procedure.input().handle(async () => {
+ try {
+ return ProviderFactory.getAvailableProviders();
+ } catch (error) {
+ console.error('Failed to get available providers:', error);
+ return [];
+ }
+ }),
+ abortRequest: t.procedure
+ .input<{ requestId: string }>()
+ .handle(async ({ input }) => {
+ try {
+ const llm = createLLM(currentLLMConfigRef.current);
+ llm.abortRequest(input.requestId);
+ return true;
+ } catch (error) {
+ console.error('Failed to abort request:', error);
+ return false;
+ }
+ }),
+});
diff --git a/apps/agent-tars/src/main/ipcRoutes/search.ts b/apps/agent-tars/src/main/ipcRoutes/search.ts
new file mode 100644
index 000000000..2d07f5663
--- /dev/null
+++ b/apps/agent-tars/src/main/ipcRoutes/search.ts
@@ -0,0 +1,19 @@
+import { initIpc } from '@ui-tars/electron-ipc/main';
+import { updateSearchConfig } from '../customTools/search';
+import { SearchSettings } from '@agent-infra/shared';
+
+const t = initIpc.create();
+
+export const searchRoute = t.router({
+ updateSearchConfig: t.procedure
+ .input()
+ .handle(async ({ input }) => {
+ try {
+ await updateSearchConfig(input);
+ return true;
+ } catch (error) {
+ console.error('Failed to update search configuration:', error);
+ return false;
+ }
+ }),
+});
diff --git a/apps/agent-tars/src/main/llmProvider/ProviderFactory.ts b/apps/agent-tars/src/main/llmProvider/ProviderFactory.ts
new file mode 100644
index 000000000..7967a20b5
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/ProviderFactory.ts
@@ -0,0 +1,107 @@
+import { LLMConfig, LLMProvider } from './interfaces/LLMProvider';
+import { OpenAIProvider } from './providers/OpenAIProvider';
+import { AnthropicProvider } from './providers/AnthropicProvider';
+import { AzureOpenAIProvider } from './providers/AzureOpenAIProvider';
+import { GeminiProvider } from './providers/GeminiProvider';
+import { MistralProvider } from './providers/MistralProvider';
+
+// Define model prefixes that will be used to determine the provider
+const MODEL_PREFIXES = {
+ OPENAI: ['gpt-', 'o1', 'o3', 'davinci'],
+ ANTHROPIC: ['claude'],
+ AZURE_OPENAI: ['aws_', 'azure_'],
+ GEMINI: ['gemini'],
+ MISTRAL: ['mistral'],
+};
+
+/**
+ * Factory for creating LLM provider instances based on model or explicit provider selection
+ */
+export class ProviderFactory {
+ /**
+ * Create a provider instance based on model name or explicit provider selection
+ * @param config LLM configuration including model
+ * @param providerName Optional explicit provider name to use
+ * @returns Instance of LLMProvider
+ */
+ static createProvider(config: LLMConfig, providerName?: string): LLMProvider {
+ // If provider name is explicitly specified, use that
+ if (providerName) {
+ return ProviderFactory.createProviderByName(providerName, config);
+ }
+
+ const DEFAULT_MODEL = 'claude-3.7-sonnet-latest';
+ // Otherwise, determine provider from model name
+ const model = config.model?.toLowerCase() || DEFAULT_MODEL;
+
+ if (!model) {
+ // Default to OpenAI if no model is specified
+ return new OpenAIProvider(config);
+ }
+
+ // Check model prefix to determine provider
+ if (MODEL_PREFIXES.OPENAI.some((prefix) => model.startsWith(prefix))) {
+ return new OpenAIProvider(config);
+ }
+
+ if (MODEL_PREFIXES.ANTHROPIC.some((prefix) => model.startsWith(prefix))) {
+ return new AnthropicProvider(config);
+ }
+
+ if (
+ MODEL_PREFIXES.AZURE_OPENAI.some((prefix) => model.startsWith(prefix))
+ ) {
+ return new AzureOpenAIProvider(config);
+ }
+
+ if (MODEL_PREFIXES.GEMINI.some((prefix) => model.startsWith(prefix))) {
+ return new GeminiProvider(config);
+ }
+
+ if (MODEL_PREFIXES.MISTRAL.some((prefix) => model.startsWith(prefix))) {
+ return new MistralProvider(config);
+ }
+
+ // Default to OpenAI if model doesn't match any known prefix
+ console.warn(
+ `Unknown model prefix: ${model}. Defaulting to OpenAI provider.`,
+ );
+ return new OpenAIProvider(config);
+ }
+
+ /**
+ * Create a provider instance by explicit provider name
+ * @param providerName Provider name to use
+ * @param config LLM configuration
+ * @returns Instance of LLMProvider
+ */
+ private static createProviderByName(
+ providerName: string,
+ config: LLMConfig,
+ ): LLMProvider {
+ switch (providerName.toLowerCase()) {
+ case 'openai':
+ return new OpenAIProvider(config);
+ case 'anthropic':
+ return new AnthropicProvider(config);
+ case 'azure':
+ case 'azure_openai':
+ return new AzureOpenAIProvider(config);
+ case 'gemini':
+ case 'google':
+ return new GeminiProvider(config);
+ case 'mistral':
+ return new MistralProvider(config);
+ default:
+ throw new Error(`Unknown provider name: ${providerName}`);
+ }
+ }
+
+ /**
+ * Get a list of available provider names
+ * @returns Array of provider names
+ */
+ static getAvailableProviders(): string[] {
+ return ['openai', 'anthropic', 'azure_openai'];
+ }
+}
diff --git a/apps/agent-tars/src/main/llmProvider/README.md b/apps/agent-tars/src/main/llmProvider/README.md
new file mode 100644
index 000000000..93be71882
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/README.md
@@ -0,0 +1,265 @@
+# Unified LLM Provider Interface
+
+This module provides a unified interface for working with different Large Language Model (LLM) providers, including:
+
+- OpenAI
+- Anthropic (Claude)
+- Azure OpenAI
+- Google Gemini
+- Mistral
+
+## Features
+
+- Unified API for different LLM providers
+- Automatic provider selection based on model name
+- Explicit provider selection
+- Simple configuration via environment variables or code
+- Support for streaming responses
+- Support for function/tool calling
+- Abort mechanism for ongoing requests
+
+## Installation
+
+This module is part of the Open Agent project. No additional installation is required if you're already using the project.
+
+However, you need to install the necessary dependencies for each provider you want to use:
+
+```bash
+# For OpenAI
+npm install openai
+
+# For Anthropic
+npm install @anthropic-ai/sdk
+
+# For Google Gemini
+npm install @google/generative-ai
+
+# For Mistral
+npm install @mistralai/mistralai
+```
+
+## Configuration
+
+### Environment Variables
+
+Configure your API keys and default models in the `.env` file:
+
+```bash
+# OpenAI API Configuration
+OPENAI_API_KEY=your_openai_api_key_here
+OPENAI_API_BASE_URL=your_openai_base_url_here # Optional
+OPENAI_DEFAULT_MODEL=gpt-4o # Optional, defaults to gpt-4o
+
+# Azure OpenAI API Configuration
+AZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint_here
+AZURE_OPENAI_API_VERSION=your_azure_openai_api_version_here
+AZURE_OPENAI_MODEL=your_azure_openai_model_here
+AZURE_OPENAI_API_KEY=your_azure_openai_api_key_here
+
+# Anthropic API Configuration
+ANTHROPIC_API_KEY=your_anthropic_api_key_here
+ANTHROPIC_API_BASE_URL=your_anthropic_base_url_here # Optional
+ANTHROPIC_DEFAULT_MODEL=claude-3-sonnet-20240229 # Optional
+
+# Google Gemini API Configuration
+GEMINI_API_KEY=your_gemini_api_key_here
+GEMINI_API_BASE_URL=your_gemini_base_url_here # Optional
+GEMINI_DEFAULT_MODEL=gemini-1.5-pro # Optional
+
+# Mistral API Configuration
+MISTRAL_API_KEY=your_mistral_api_key_here
+MISTRAL_API_BASE_URL=your_mistral_base_url_here # Optional
+MISTRAL_DEFAULT_MODEL=mistral-large-latest # Optional
+```
+
+### Code Configuration
+
+Alternatively, you can configure the provider in code:
+
+```typescript
+import { createLLM, LLMConfig } from './llmProvider';
+
+const config: LLMConfig = {
+ model: 'claude-3-sonnet-20240229',
+ apiKey: 'your_api_key_here',
+ temperature: 0.7,
+ maxTokens: 4000,
+ topP: 1.0,
+ // ...other options
+};
+
+const llm = createLLM(config);
+```
+
+## Usage
+
+### Basic Usage with the Default Instance
+
+```typescript
+import { llm } from './llmProvider';
+
+async function main() {
+ const response = await llm.askLLMText({
+ messages: [{ role: 'user', content: 'Hello, how are you?' }],
+ requestId: 'request-1',
+ });
+
+ console.log(response);
+}
+```
+
+### Creating a Custom Instance
+
+```typescript
+import { createLLM, LLMConfig } from './llmProvider';
+
+// Create a new LLM instance with specific configuration
+const config: LLMConfig = {
+ model: 'gpt-4o',
+ temperature: 0.7,
+ maxTokens: 2000,
+};
+
+const customLLM = createLLM(config);
+```
+
+### Specifying the Provider Explicitly
+
+```typescript
+import { createLLM } from './llmProvider';
+
+// Create a new LLM instance with a specific provider
+const mistralLLM = createLLM({ model: 'mistral-large-latest' }, 'mistral');
+```
+
+### Switching Providers at Runtime
+
+```typescript
+import { createLLM } from './llmProvider';
+
+const dynamicLLM = createLLM({ model: 'gpt-4o' });
+
+// Later, switch to a different provider
+dynamicLLM.setProvider({ model: 'claude-3-opus-20240229' });
+```
+
+### Using Streaming Responses
+
+```typescript
+import { llm } from './llmProvider';
+
+async function streamingExample() {
+ const stream = llm.askLLMTextStream({
+ messages: [{ role: 'user', content: 'Write a short story about a robot.' }],
+ requestId: 'stream-1',
+ });
+
+ for await (const chunk of stream) {
+ process.stdout.write(chunk); // Print each chunk as it arrives
+ }
+}
+```
+
+### Using Tool Calls (Function Calling)
+
+```typescript
+import { llm } from './llmProvider';
+import { ChatCompletionTool } from 'openai/resources';
+
+async function toolExample() {
+ // Define tools in OpenAI format
+ const tools: ChatCompletionTool[] = [
+ {
+ type: 'function',
+ function: {
+ name: 'get_weather',
+ description: 'Get the current weather in a location',
+ parameters: {
+ type: 'object',
+ properties: {
+ location: {
+ type: 'string',
+ description: 'The city and state, e.g., San Francisco, CA',
+ },
+ unit: {
+ type: 'string',
+ enum: ['celsius', 'fahrenheit'],
+ description: 'The unit of temperature',
+ },
+ },
+ required: ['location'],
+ },
+ },
+ },
+ ];
+
+ const response = await llm.askTool({
+ messages: [{ role: 'user', content: 'What is the weather like in New York?' }],
+ tools,
+ requestId: 'tool-1',
+ toolChoice: 'auto', // 'auto', 'none', or 'required'
+ });
+
+ console.log('Content:', response.content);
+ console.log('Tool calls:', response.tool_calls);
+}
+```
+
+### Aborting Requests
+
+```typescript
+import { llm } from './llmProvider';
+
+const requestId = 'request-to-abort';
+
+// Start a request
+const promise = llm.askLLMText({
+ messages: [{ role: 'user', content: 'Write a very long essay about quantum physics' }],
+ requestId,
+});
+
+// Later, abort the request
+setTimeout(() => {
+ llm.abortRequest(requestId);
+ console.log('Request aborted');
+}, 1000);
+```
+
+## Available Models
+
+To get a list of available providers and models:
+
+```typescript
+import { LLM } from './llmProvider';
+
+console.log('Available providers:', LLM.getAvailableProviders());
+console.log('Supported models:', LLM.getSupportedModels());
+```
+
+## Example
+
+See `example.ts` for a complete example of using the unified LLM provider interface.
+
+## Architecture
+
+The unified LLM provider interface consists of:
+
+- **LLM**: The main class that provides a unified interface for all providers
+- **BaseProvider**: An abstract base class for all provider implementations
+- **Provider implementations**: Concrete implementations for each LLM provider (OpenAI, Anthropic, etc.)
+- **ProviderFactory**: A factory class for creating provider instances based on configuration
+
+## Extending with New Providers
+
+To add a new provider, create a new class that extends `BaseProvider` and implements the required methods. Then, update the `ProviderFactory` to include the new provider.
+
+For example, to add a new provider called "NewProvider":
+
+1. Create a new file `providers/NewProvider.ts` extending `BaseProvider`
+2. Implement the required methods (`formatMessages`, `askLLMText`, `askTool`, `askLLMTextStream`)
+3. Update `ProviderFactory.ts` to include the new provider
+4. Update `.env` with any necessary environment variables for the new provider
+
+## License
+
+This module is part of the Open Agent project and is licensed under the same terms.
diff --git a/apps/agent-tars/src/main/llmProvider/config/config.ts b/apps/agent-tars/src/main/llmProvider/config/config.ts
new file mode 100644
index 000000000..677457dbc
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/config/config.ts
@@ -0,0 +1,29 @@
+import dotenv from 'dotenv';
+
+// Load environment variables
+dotenv.config();
+
+/**
+ * LLM Provider types
+ */
+export enum ProviderType {
+ OPENAI = 'openai',
+ AZURE_OPENAI = 'azure_openai',
+ ANTHROPIC = 'anthropic',
+ GOOGLE = 'google',
+ MISTRAL = 'mistral',
+}
+
+/**
+ * Configuration interface for each provider
+ */
+export interface ProviderConfig {
+ enabled: boolean;
+ apiKey?: string;
+ baseURL?: string;
+ apiVersion?: string;
+ models: {
+ [key: string]: string;
+ };
+ defaultModel: string;
+}
diff --git a/apps/agent-tars/src/main/llmProvider/example.ts b/apps/agent-tars/src/main/llmProvider/example.ts
new file mode 100644
index 000000000..457847c20
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/example.ts
@@ -0,0 +1,88 @@
+/**
+ * Example usage of the unified LLM provider interface
+ *
+ * This file demonstrates how to use the LLM provider interface
+ * with different models from different providers.
+ */
+
+// import { Message } from '@agent-infra/shared';
+// import { LLM, createLLM, LLMConfig } from './index';
+
+// async function main() {
+// // Example 1: Using the default singleton LLM instance
+// console.log('Example 1: Using the default LLM instance');
+// const response1 = await llm.askLLMText({
+// messages: [Message.userMessage('Hello, how are you?')],
+// requestId: 'example1',
+// });
+// console.log('Response:', response1);
+// // Example 2: Creating a custom LLM instance with a specific model
+// console.log('\nExample 2: Using Claude from Anthropic');
+// const claudeConfig: LLMConfig = {
+// model: 'claude-3-7-sonnet-latest',
+// temperature: 0.7,
+// };
+// const claudeLLM = createLLM(claudeConfig);
+// const response2 = await claudeLLM.askLLMText({
+// messages: [Message.userMessage('Tell me about Claude')],
+// requestId: 'example2',
+// });
+// console.log('Response:', response2);
+// // Example 3: Creating a custom LLM instance with a specific provider
+// console.log('\nExample 3: Using Azure OpenAI from Azure');
+// const awsConfig: LLMConfig = {
+// model: 'aws_claude35_sdk_sonnet_v2',
+// temperature: 0.5,
+// };
+// const awsLLM = createLLM(awsConfig);
+// const response3 = await awsLLM.askLLMText({
+// messages: [Message.userMessage('Tell me about claude')],
+// requestId: 'example3',
+// });
+// console.log('Response:', response3);
+// // Example 4: Switching models/providers at runtime
+// console.log('\nExample 4: Switching providers at runtime');
+// const dynamicLLM = createLLM({ model: 'gpt-4o-mini' });
+// const response4a = await dynamicLLM.askLLMText({
+// messages: [Message.userMessage('What model are you using?')],
+// requestId: 'example4a',
+// });
+// console.log('Response with GPT-4o:', response4a);
+// dynamicLLM.setProvider({ model: 'gemini-2.0-flash' });
+// const response4b = await dynamicLLM.askTool({
+// messages: [Message.userMessage('What model are you using now?')],
+// requestId: 'example4b',
+// tools: [
+// {
+// type: 'function',
+// function: {
+// name: 'get_model_name',
+// description: 'Get the name of the current model',
+// parameters: {
+// type: 'object',
+// properties: {
+// model: {
+// type: 'string',
+// description: 'The name of the model',
+// },
+// },
+// },
+// },
+// },
+// ],
+// });
+// console.log('Response with model:', JSON.stringify(response4b, null, 2));
+// // Switch to Mistral
+// dynamicLLM.setProvider({ model: 'mistral-large-latest' });
+// const response4c = await dynamicLLM.askLLMText({
+// messages: [Message.userMessage('What model are you using now?')],
+// requestId: 'example4b',
+// });
+// console.log('Response with Mistral:', response4c);
+// // Example 5: Getting available providers and models
+// console.log('\nExample 5: Available providers and models');
+// console.log('Available providers:', LLM.getAvailableProviders());
+// }
+
+// // Run the example - uncomment to execute
+// main().catch(console.error);
diff --git a/apps/agent-tars/src/main/llmProvider/index.ts b/apps/agent-tars/src/main/llmProvider/index.ts
new file mode 100644
index 000000000..b5321301f
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/index.ts
@@ -0,0 +1,140 @@
+import dotenv from 'dotenv';
+import { MCPServerName, Message } from '@agent-infra/shared';
+import { ChatCompletionTool } from 'openai/resources/index.mjs';
+import { mapToolKeysToAzureTools } from '../../main/mcp/tools';
+import { createMcpClient } from '../../main/mcp/client';
+import { listCustomTools } from '../../main/customTools';
+import { ProviderFactory } from './ProviderFactory';
+import {
+ LLMConfig,
+ LLMProvider,
+ LLMResponse,
+ ToolChoice,
+} from './interfaces/LLMProvider';
+
+// Load environment variables
+dotenv.config();
+
+// Export interfaces and types from LLMProvider for convenience
+export type {
+ LLMConfig,
+ LLMResponse,
+ ToolChoice,
+ LLMProvider,
+} from './interfaces/LLMProvider';
+
+/**
+ * Unified LLM class that uses different providers based on configuration
+ */
+export class LLM {
+ private provider: LLMProvider;
+ private config: LLMConfig;
+
+ constructor(config: LLMConfig = {}) {
+ this.config = config;
+ // Create provider instance based on the model or explicit provider setting
+ this.provider = ProviderFactory.createProvider(config, config.configName);
+ }
+
+ /**
+ * Get the current provider
+ */
+ getProvider(): LLMProvider {
+ return this.provider;
+ }
+
+ /**
+ * Change the provider or model at runtime
+ */
+ setProvider(config: LLMConfig = {}, providerName?: string): void {
+ this.config = { ...this.config, ...config };
+ this.provider = ProviderFactory.createProvider(
+ this.config,
+ providerName || config.configName,
+ );
+ }
+
+ /**
+ * Send a message to the LLM and get a response
+ */
+ async askLLMText({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): Promise {
+ return this.provider.askLLMText({ messages, requestId });
+ }
+
+ /**
+ * Send a message to the LLM with tools and get a response with potential tool calls
+ */
+ async askTool({
+ messages,
+ tools,
+ mcpServerKeys,
+ requestId,
+ toolChoice,
+ }: {
+ messages: Message[];
+ tools: ChatCompletionTool[];
+ mcpServerKeys?: MCPServerName[];
+ requestId: string;
+ toolChoice?: ToolChoice;
+ }): Promise {
+ try {
+ const mcpClient = await createMcpClient();
+ const mcpTools = await mcpClient.listTools();
+ const customTools = listCustomTools();
+ const normalizeMcpTools = mapToolKeysToAzureTools(
+ mcpTools,
+ mcpServerKeys || [],
+ );
+
+ const allTools = [...tools, ...normalizeMcpTools, ...customTools];
+ return this.provider.askTool({
+ messages,
+ tools: allTools,
+ requestId,
+ toolChoice: toolChoice || 'auto',
+ });
+ } catch (error: any) {
+ throw new Error(`Failed to get tool response from LLM: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM and get a streaming response
+ */
+ async *askLLMTextStream({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): AsyncGenerator {
+ yield* this.provider.askLLMTextStream({ messages, requestId });
+ }
+
+ /**
+ * Abort an active request
+ */
+ abortRequest(requestId: string): boolean {
+ if ('abortRequest' in this.provider) {
+ return this.provider.abortRequest(requestId);
+ }
+ return false;
+ }
+
+ /**
+ * Get a list of available providers
+ */
+ static getAvailableProviders(): string[] {
+ return ProviderFactory.getAvailableProviders();
+ }
+}
+
+export function createLLM(config: LLMConfig = {}) {
+ return new LLM(config);
+}
diff --git a/apps/agent-tars/src/main/llmProvider/interfaces/LLMProvider.ts b/apps/agent-tars/src/main/llmProvider/interfaces/LLMProvider.ts
new file mode 100644
index 000000000..7655b1929
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/interfaces/LLMProvider.ts
@@ -0,0 +1,67 @@
+import { Message, ToolCall } from '@agent-infra/shared';
+import { ChatCompletionTool } from 'openai/resources';
+
+/**
+ * Common interface for all LLM provider configurations
+ */
+export interface LLMConfig {
+ model?: string;
+ apiKey?: string;
+ temperature?: number;
+ maxTokens?: number;
+ topP?: number;
+ frequencyPenalty?: number;
+ presencePenalty?: number;
+ configName?: string;
+ baseURL?: string;
+ apiVersion?: string;
+}
+
+/**
+ * Standardized response format from LLM providers
+ */
+export interface LLMResponse {
+ content: string | null;
+ tool_calls?: ToolCall[];
+}
+
+/**
+ * Defines how tools should be chosen by the LLM
+ */
+export type ToolChoice = 'none' | 'auto' | 'required';
+
+/**
+ * Common interface for all LLM providers
+ */
+export interface LLMProvider {
+ /**
+ * Send a message to the LLM and get a text response
+ */
+ askLLMText(params: {
+ messages: Message[];
+ requestId: string;
+ }): Promise;
+
+ /**
+ * Send a message to the LLM with tools and get a response with potential tool calls
+ */
+ askTool(params: {
+ messages: Message[];
+ tools: ChatCompletionTool[];
+ requestId: string;
+ toolChoice?: ToolChoice;
+ }): Promise;
+
+ /**
+ * Send a message to the LLM and get a streaming response
+ */
+ askLLMTextStream(params: {
+ messages: Message[];
+ requestId: string;
+ }): AsyncGenerator;
+
+ /**
+ * Abort an active request
+ */
+ abortRequest(requestId: string): boolean;
+}
diff --git a/apps/agent-tars/src/main/llmProvider/providers/AnthropicProvider.ts b/apps/agent-tars/src/main/llmProvider/providers/AnthropicProvider.ts
new file mode 100644
index 000000000..dedbcec2c
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/providers/AnthropicProvider.ts
@@ -0,0 +1,350 @@
+import Anthropic from '@anthropic-ai/sdk';
+import { Message, ToolCall } from '@agent-infra/shared';
+import { ChatCompletionTool } from 'openai/resources';
+import { BaseProvider } from './BaseProvider';
+import { LLMConfig, LLMResponse, ToolChoice } from '../interfaces/LLMProvider';
+
+/**
+ * Helper to convert OpenAI tool format to Anthropic tool format
+ */
+function convertOpenAIToolsToAnthropic(
+ tools: ChatCompletionTool[],
+): Anthropic.Tool[] {
+ return tools.map((tool) => {
+ if (tool.type === 'function') {
+ return {
+ name: tool.function.name,
+ description: tool.function.description,
+ input_schema: tool.function.parameters as any,
+ };
+ }
+ throw new Error(`Unsupported tool type: ${tool.type}`);
+ });
+}
+
+/**
+ * Anthropic provider implementation
+ */
+export class AnthropicProvider extends BaseProvider {
+ private client: Anthropic;
+ private model: string;
+
+ constructor(config: LLMConfig = {}) {
+ super(config);
+
+ // Use environment variables or defaults
+ const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY || '';
+ const baseURL = config.baseURL || process.env.ANTHROPIC_API_BASE_URL || '';
+
+ if (!apiKey) {
+ throw new Error(
+ 'Anthropic API key is required. Set ANTHROPIC_API_KEY environment variable or provide in config.',
+ );
+ }
+
+ this.client = new Anthropic({
+ apiKey,
+ ...(baseURL && { baseURL }),
+ });
+
+ this.model =
+ config.model ||
+ process.env.ANTHROPIC_DEFAULT_MODEL ||
+ 'claude-3-7-sonnet-latest';
+ }
+
+ /**
+ * Convert Message objects to Anthropic API format
+ */
+ protected formatMessages(messages: Message[]): Anthropic.MessageParam[] {
+ // Need to convert the messages from OpenAI format to Anthropic format
+ const anthropicMessages: Anthropic.MessageParam[] = [];
+
+ // First, extract system message if present
+ const systemMsgIndex = messages.findIndex((m) => m.role === 'system');
+ if (systemMsgIndex >= 0) {
+ // Remove system message as it's handled separately
+ messages = [
+ ...messages.slice(0, systemMsgIndex),
+ ...messages.slice(systemMsgIndex + 1),
+ ];
+ }
+
+ // Convert remaining messages to Anthropic format
+ for (const msg of messages) {
+ if (msg.role === 'user') {
+ anthropicMessages.push({
+ role: 'user',
+ content: msg.content || '',
+ });
+ } else if (msg.role === 'assistant') {
+ const assistantMsg: Anthropic.MessageParam = {
+ role: 'assistant',
+ content: msg.content || '',
+ };
+
+ // Handle tool calls
+ if (msg.tool_calls) {
+ const content: Anthropic.ContentBlock[] = [
+ { type: 'text', text: msg.content || '', citations: [] },
+ ];
+
+ // Add tool calls
+ for (const toolCall of msg.tool_calls) {
+ content.push({
+ type: 'tool_use',
+ id: toolCall.id,
+ name: toolCall.function.name,
+ input: JSON.parse(toolCall.function.arguments || '{}'),
+ });
+ }
+
+ assistantMsg.content = content;
+ }
+
+ anthropicMessages.push(assistantMsg);
+ } else if (msg.role === 'tool') {
+ // Find the last assistant message
+ const lastAssistantIdx = anthropicMessages.length - 1;
+ if (
+ lastAssistantIdx >= 0 &&
+ anthropicMessages[lastAssistantIdx].role === 'assistant'
+ ) {
+ // Add tool output to the assistant's message content
+ const assistantMsg = anthropicMessages[lastAssistantIdx];
+ const content = Array.isArray(assistantMsg.content)
+ ? assistantMsg.content
+ : [];
+
+ content.push({
+ type: 'tool_result',
+ tool_use_id: msg.tool_call_id!,
+ content: msg.content || '',
+ });
+
+ anthropicMessages[lastAssistantIdx].content = content;
+ }
+ }
+ }
+
+ return anthropicMessages;
+ }
+
+ /**
+ * Process tool calls from Anthropic response
+ */
+ private processToolCalls(
+ response: Anthropic.Message,
+ ): ToolCall[] | undefined {
+ if (!response.content || !Array.isArray(response.content)) {
+ return undefined;
+ }
+
+ const toolUses = response.content.filter(
+ (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use',
+ );
+
+ if (toolUses.length === 0) {
+ return undefined;
+ }
+
+ return toolUses.map((toolUse) => ({
+ id: toolUse.id,
+ type: 'function' as const,
+ function: {
+ name: toolUse.name,
+ arguments: JSON.stringify(toolUse.input),
+ },
+ }));
+ }
+
+ /**
+ * Extract text content from Anthropic response
+ */
+ private extractTextContent(response: Anthropic.Message): string {
+ if (typeof response.content === 'string') {
+ return response.content;
+ }
+
+ if (Array.isArray(response.content)) {
+ const textBlocks = response.content.filter(
+ (block): block is Anthropic.TextBlock => block.type === 'text',
+ );
+ return textBlocks.map((block) => block.text).join('');
+ }
+
+ return '';
+ }
+
+ /**
+ * Send a message to the LLM and get a response
+ */
+ async askLLMText({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ // Extract system message if present at the beginning
+ let systemMessage = '';
+ const systemMsgIndex = messages.findIndex((m) => m.role === 'system');
+ if (systemMsgIndex >= 0) {
+ systemMessage = messages[systemMsgIndex].content || '';
+ }
+
+ const response = await this.client.messages.create(
+ {
+ model: this.model,
+ messages: formattedMessages,
+ system: systemMessage,
+ temperature: this.config.temperature,
+ max_tokens: this.config.maxTokens || 8192,
+ top_p: this.config.topP,
+ },
+ {
+ signal: controller.signal,
+ },
+ );
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+ this.cleanupRequest(requestId);
+ return this.extractTextContent(response);
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return '';
+ }
+ throw new Error(`Failed to get response from Anthropic: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM with tools and get a response with potential tool calls
+ */
+ async askTool({
+ messages,
+ tools,
+ requestId,
+ toolChoice = 'auto',
+ }: {
+ messages: Message[];
+ tools: ChatCompletionTool[];
+ requestId: string;
+ toolChoice?: ToolChoice;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ // Extract system message if present
+ let systemMessage = '';
+ const systemMsgIndex = messages.findIndex((m) => m.role === 'system');
+ if (systemMsgIndex >= 0) {
+ systemMessage = messages[systemMsgIndex].content || '';
+ }
+
+ // Convert tools to Anthropic format
+ const anthropicTools = convertOpenAIToolsToAnthropic(tools);
+
+ const response = await this.client.messages.create(
+ {
+ model: this.model,
+ messages: formattedMessages,
+ system: systemMessage,
+ temperature: this.config.temperature,
+ max_tokens: this.config.maxTokens || 4000,
+ top_p: this.config.topP,
+ tools: anthropicTools,
+ tool_choice:
+ toolChoice === 'required' ? { type: 'any' } : { type: 'auto' },
+ },
+ {
+ signal: controller.signal,
+ },
+ );
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ this.cleanupRequest(requestId);
+ const content = this.extractTextContent(response);
+ const toolCalls = this.processToolCalls(response);
+ return { content, tool_calls: toolCalls };
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return { content: '' };
+ }
+ throw new Error(`Failed to get tool response from Anthropic: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM and get a streaming response
+ */
+ async *askLLMTextStream({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): AsyncGenerator {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ // Extract system message if present
+ let systemMessage = '';
+ const systemMsgIndex = messages.findIndex((m) => m.role === 'system');
+ if (systemMsgIndex >= 0) {
+ systemMessage = messages[systemMsgIndex].content || '';
+ }
+
+ const stream = await this.client.messages.create(
+ {
+ model: this.model,
+ messages: formattedMessages,
+ system: systemMessage,
+ temperature: this.config.temperature,
+ max_tokens: this.config.maxTokens || 4000,
+ top_p: this.config.topP,
+ stream: true,
+ },
+ {
+ signal: controller.signal,
+ },
+ );
+
+ for await (const chunk of stream) {
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ if (
+ chunk.type === 'content_block_delta' &&
+ chunk.delta?.type === 'text_delta'
+ ) {
+ yield chunk.delta.text;
+ }
+ }
+ } catch (error: any) {
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ throw new Error('Request aborted');
+ }
+ throw error;
+ } finally {
+ this.cleanupRequest(requestId);
+ }
+ }
+}
diff --git a/apps/agent-tars/src/main/llmProvider/providers/AzureOpenAIProvider.ts b/apps/agent-tars/src/main/llmProvider/providers/AzureOpenAIProvider.ts
new file mode 100644
index 000000000..2d47363fa
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/providers/AzureOpenAIProvider.ts
@@ -0,0 +1,235 @@
+import OpenAI, { AzureOpenAI } from 'openai';
+import { Message, ToolCall } from '@agent-infra/shared';
+import { ChatCompletionTool } from 'openai/resources';
+
+import { BaseProvider } from './BaseProvider';
+import { LLMConfig, LLMResponse, ToolChoice } from '../interfaces/LLMProvider';
+
+/**
+ * Azure OpenAI provider implementation
+ */
+export class AzureOpenAIProvider extends BaseProvider {
+ private client: AzureOpenAI;
+ private model: string;
+
+ constructor(config: LLMConfig = {}) {
+ super(config);
+
+ // Use environment variables or defaults
+ const apiKey =
+ config.apiKey || process.env.AZURE_OPENAI_API_KEY || 'your-api-key';
+ const endpoint =
+ config.baseURL || process.env.AZURE_OPENAI_ENDPOINT || 'your-endpoint';
+ const apiVersion =
+ config.apiVersion ||
+ process.env.AZURE_OPENAI_API_VERSION ||
+ '2023-12-01-preview';
+
+ if (!apiKey) {
+ throw new Error(
+ 'Azure OpenAI API key is required. Set AZURE_OPENAI_API_KEY environment variable or provide in config.',
+ );
+ }
+
+ if (!endpoint) {
+ throw new Error(
+ 'Azure OpenAI endpoint is required. Set AZURE_OPENAI_ENDPOINT environment variable or provide in config.',
+ );
+ }
+
+ this.client = new AzureOpenAI({ apiKey, apiVersion, endpoint });
+ this.model = config.model || process.env.AZURE_OPENAI_MODEL || 'gpt-4o';
+ }
+
+ /**
+ * Convert Message objects to Azure OpenAI API format
+ */
+ protected formatMessages(messages: Message[]): any[] {
+ return messages.map((item) => ({
+ role: item.role as any,
+ content: item.content,
+ ...(item.tool_call_id && { tool_call_id: item.tool_call_id }),
+ ...(item.tool_calls && { tool_calls: item.tool_calls }),
+ ...(item.name && { name: item.name }),
+ }));
+ }
+
+ /**
+ * Process tool calls from response
+ */
+ private processToolCalls(
+ response: OpenAI.Chat.ChatCompletion,
+ ): ToolCall[] | undefined {
+ const toolCalls = response.choices
+ .filter((choice) => choice.finish_reason === 'tool_calls')
+ .map((choice) => choice.message.tool_calls)
+ .flat();
+
+ if (!toolCalls || toolCalls.length === 0) return undefined;
+
+ return toolCalls
+ .map((toolCall) => {
+ if (!toolCall) {
+ return undefined;
+ }
+ return {
+ id: toolCall.id,
+ type: 'function' as const,
+ function: {
+ name: toolCall.function.name,
+ arguments: toolCall.function.arguments,
+ },
+ };
+ })
+ .filter(Boolean) as ToolCall[];
+ }
+
+ /**
+ * Send a message to the LLM and get a response
+ */
+ async askLLMText({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ const response = await this.client.chat.completions.create(
+ {
+ model: this.model,
+ messages: formattedMessages,
+ temperature: this.config.temperature,
+ max_tokens: this.config.maxTokens,
+ top_p: this.config.topP,
+ frequency_penalty: this.config.frequencyPenalty,
+ presence_penalty: this.config.presencePenalty,
+ },
+ {
+ signal: controller.signal,
+ },
+ );
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+ this.cleanupRequest(requestId);
+ return response.choices[0].message.content || '';
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return '';
+ }
+ throw new Error(`Failed to get response from Azure OpenAI: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM with tools and get a response with potential tool calls
+ */
+ async askTool({
+ messages,
+ tools,
+ requestId,
+ toolChoice = 'auto',
+ }: {
+ messages: Message[];
+ tools: ChatCompletionTool[];
+ requestId: string;
+ toolChoice?: ToolChoice;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ const response = await this.client.chat.completions.create(
+ {
+ model: this.model,
+ messages: formattedMessages,
+ temperature: this.config.temperature,
+ max_tokens: this.config.maxTokens,
+ tools,
+ tool_choice: toolChoice,
+ top_p: this.config.topP,
+ frequency_penalty: this.config.frequencyPenalty,
+ presence_penalty: this.config.presencePenalty,
+ },
+ {
+ signal: controller.signal,
+ },
+ );
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ this.cleanupRequest(requestId);
+ const content = response.choices[0].message.content;
+ const toolCalls = this.processToolCalls(response);
+ return { content, tool_calls: toolCalls };
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return { content: '' };
+ }
+ throw new Error(
+ `Failed to get tool response from Azure OpenAI: ${error}`,
+ );
+ }
+ }
+
+ /**
+ * Send a message to the LLM and get a streaming response
+ */
+ async *askLLMTextStream({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): AsyncGenerator {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ const stream = await this.client.chat.completions.create(
+ {
+ model: this.model,
+ messages: formattedMessages,
+ temperature: this.config.temperature,
+ max_tokens: this.config.maxTokens,
+ top_p: this.config.topP,
+ frequency_penalty: this.config.frequencyPenalty,
+ presence_penalty: this.config.presencePenalty,
+ stream: true,
+ },
+ {
+ signal: controller.signal,
+ },
+ );
+
+ for await (const chunk of stream) {
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+ const content = chunk.choices[0]?.delta?.content;
+ if (content) {
+ yield content;
+ }
+ }
+ } catch (error: any) {
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ throw new Error('Request aborted');
+ }
+ throw error;
+ } finally {
+ this.cleanupRequest(requestId);
+ }
+ }
+}
diff --git a/apps/agent-tars/src/main/llmProvider/providers/BaseProvider.ts b/apps/agent-tars/src/main/llmProvider/providers/BaseProvider.ts
new file mode 100644
index 000000000..df84f6d96
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/providers/BaseProvider.ts
@@ -0,0 +1,69 @@
+import { Message } from '@agent-infra/shared';
+import { LLMConfig, LLMProvider } from '../interfaces/LLMProvider';
+
+/**
+ * Base abstract class for all LLM providers
+ * Implements common functionality and requires specific provider implementations
+ */
+export abstract class BaseProvider implements LLMProvider {
+ protected config: LLMConfig;
+ protected activeRequests = new Map();
+
+ constructor(config: LLMConfig = {}) {
+ this.config = {
+ temperature: config.temperature !== undefined ? config.temperature : 0,
+ maxTokens: config.maxTokens || 4000,
+ topP: config.topP !== undefined ? config.topP : 1.0,
+ frequencyPenalty: config.frequencyPenalty || 0,
+ presencePenalty: config.presencePenalty || 0,
+ ...config,
+ };
+ }
+
+ /**
+ * Convert Message objects to provider-specific format
+ * Each provider should implement this
+ */
+ protected abstract formatMessages(messages: Message[]): any[];
+
+ /**
+ * Abort an active request
+ */
+ abortRequest(requestId: string): boolean {
+ const controller = this.activeRequests.get(requestId);
+ if (controller) {
+ controller.abort();
+ this.activeRequests.delete(requestId);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Clean up request controller after completion/error
+ */
+ protected cleanupRequest(requestId: string): void {
+ this.activeRequests.delete(requestId);
+ }
+
+ /**
+ * Implementation required by subclasses
+ */
+ abstract askLLMText(params: {
+ messages: Message[];
+ requestId: string;
+ }): Promise;
+
+ /**
+ * Implementation required by subclasses
+ */
+ abstract askTool(params: any): Promise;
+
+ /**
+ * Implementation required by subclasses
+ */
+ abstract askLLMTextStream(params: {
+ messages: Message[];
+ requestId: string;
+ }): AsyncGenerator;
+}
diff --git a/apps/agent-tars/src/main/llmProvider/providers/GeminiProvider.ts b/apps/agent-tars/src/main/llmProvider/providers/GeminiProvider.ts
new file mode 100644
index 000000000..eea65b2c8
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/providers/GeminiProvider.ts
@@ -0,0 +1,318 @@
+import { Message, ToolCall } from '@agent-infra/shared';
+import { ChatCompletionTool } from 'openai/resources';
+import {
+ GoogleGenerativeAI,
+ EnhancedGenerateContentResponse,
+ FunctionDeclaration,
+ SchemaType,
+} from '@google/generative-ai';
+import { BaseProvider } from './BaseProvider';
+import { LLMConfig, LLMResponse, ToolChoice } from '../interfaces/LLMProvider';
+
+/**
+ * Google Gemini provider implementation
+ */
+export class GeminiProvider extends BaseProvider {
+ private client: GoogleGenerativeAI;
+ private model: string;
+
+ constructor(config: LLMConfig = {}) {
+ super(config);
+
+ // Use environment variables or defaults
+ const apiKey = config.apiKey || process.env.GEMINI_API_KEY;
+
+ if (!apiKey) {
+ throw new Error(
+ 'Gemini API key is required. Set GEMINI_API_KEY environment variable or provide in config.',
+ );
+ }
+
+ // Initialize client with the actual SDK
+ this.client = new GoogleGenerativeAI(apiKey);
+
+ // Set default model or use provided one
+ this.model =
+ config.model || process.env.GEMINI_DEFAULT_MODEL || 'gemini-1.5-pro';
+ }
+
+ /**
+ * Convert Message objects to Google Gemini API format
+ */
+ protected formatMessages(messages: Message[]): any[] {
+ return messages.map((message) => {
+ const role = message.role === 'system' ? 'user' : message.role;
+ return {
+ role,
+ parts: [{ text: message.content }],
+ };
+ });
+ }
+
+ /**
+ * Process tool calls from Gemini response
+ */
+ private processToolCalls(
+ response: EnhancedGenerateContentResponse,
+ ): ToolCall[] | undefined {
+ // Check if there are any function calls in the response
+ const functionCallResults = response.functionCalls();
+ if (!functionCallResults || functionCallResults.length === 0) {
+ return undefined;
+ }
+
+ return functionCallResults.map((callResult, index) => ({
+ id: `gemini-function-call-${Date.now()}-${index}`,
+ type: 'function',
+ function: {
+ name: callResult.name,
+ arguments: callResult.args ? JSON.stringify(callResult.args) : '{}',
+ },
+ }));
+ }
+
+ /**
+ * Extract text content from Gemini response
+ */
+ private extractTextContent(
+ response: EnhancedGenerateContentResponse,
+ ): string {
+ return response.text() || '';
+ }
+
+ /**
+ * Send a message to the LLM and get a response
+ */
+ async askLLMText({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ // Create a generative model instance
+ const generativeModel = this.client.getGenerativeModel({
+ model: this.model,
+ generationConfig: {
+ temperature: this.config.temperature,
+ maxOutputTokens: this.config.maxTokens,
+ topP: this.config.topP,
+ },
+ });
+
+ // Use chat API to send messages
+ const chat = generativeModel.startChat();
+ const result = await chat.sendMessage(
+ formattedMessages.map((message) => message.parts[0].text).join('\n'),
+ {
+ signal: controller.signal,
+ },
+ );
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ this.cleanupRequest(requestId);
+ return this.extractTextContent(result.response);
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return '';
+ }
+ throw new Error(`Failed to get response from Gemini: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM with tools and get a response with potential tool calls
+ */
+ async askTool({
+ messages,
+ tools,
+ requestId,
+ }: {
+ messages: Message[];
+ tools: ChatCompletionTool[];
+ requestId: string;
+ toolChoice?: ToolChoice;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ // Convert OpenAI-style tools to Gemini function declarations
+ const functionDeclarations: FunctionDeclaration[] = tools.map((tool) => ({
+ name: tool.function.name,
+ description: tool.function.description || '',
+ parameters: tool.function.parameters
+ ? {
+ type: SchemaType.OBJECT,
+ properties: Object.entries(
+ tool.function.parameters.properties || {},
+ ).reduce(
+ (acc, [key, value]) => {
+ // Convert each property to a format Gemini expects
+ acc[key] = convertOpenAISchemaToGemini(value);
+ return acc;
+ },
+ {
+ // Add an extra properties to avoid runtime error for gemeni
+ message: {
+ type: 'string',
+ description: 'the user input message',
+ },
+ } as Record,
+ ),
+ // Copy over required fields if present
+ }
+ : undefined,
+ }));
+
+ function convertOpenAISchemaToGemini(schema: any): any {
+ if (!schema) return {};
+
+ // Basic conversion based on type
+ if (schema.type === 'object') {
+ return {
+ type: SchemaType.OBJECT,
+ properties: Object.entries(schema.properties || {}).reduce(
+ (acc, [key, value]) => {
+ acc[key] = convertOpenAISchemaToGemini(value);
+ return acc;
+ },
+ {} as Record,
+ ),
+ ...(schema.required && { required: schema.required }),
+ ...(schema.description && { description: schema.description }),
+ };
+ } else if (schema.type === 'array') {
+ return {
+ type: SchemaType.ARRAY,
+ items: convertOpenAISchemaToGemini(schema.items),
+ ...(schema.description && { description: schema.description }),
+ };
+ } else if (schema.type === 'string') {
+ return {
+ type: SchemaType.STRING,
+ ...(schema.enum && { enum: schema.enum, format: 'enum' }),
+ ...(schema.description && { description: schema.description }),
+ };
+ } else if (schema.type === 'number') {
+ return {
+ type: SchemaType.NUMBER,
+ ...(schema.description && { description: schema.description }),
+ };
+ } else if (schema.type === 'integer') {
+ return {
+ type: SchemaType.INTEGER,
+ ...(schema.description && { description: schema.description }),
+ };
+ } else if (schema.type === 'boolean') {
+ return {
+ type: SchemaType.BOOLEAN,
+ ...(schema.description && { description: schema.description }),
+ };
+ }
+
+ // Fallback
+ return {
+ type: SchemaType.STRING,
+ ...(schema.description && { description: schema.description }),
+ };
+ }
+
+ // Create a generative model instance with tools
+ const generativeModel = this.client.getGenerativeModel({
+ model: this.model,
+ generationConfig: {
+ temperature: this.config.temperature,
+ maxOutputTokens: this.config.maxTokens,
+ topP: this.config.topP,
+ },
+ tools: [{ functionDeclarations }],
+ });
+
+ // Use chat API to send messages
+ const chat = generativeModel.startChat();
+ const result = await chat.sendMessage(
+ formattedMessages.map((message) => message.parts[0].text).join('\n'),
+ {
+ signal: controller.signal,
+ },
+ );
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ this.cleanupRequest(requestId);
+ const content = this.extractTextContent(result.response);
+ const toolCalls = this.processToolCalls(result.response);
+ return { content, tool_calls: toolCalls };
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return { content: '' };
+ }
+ throw new Error(`Failed to get tool response from Gemini: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM and get a streaming response
+ */
+ async *askLLMTextStream({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): AsyncGenerator {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ // Create a generative model instance
+ const generativeModel = this.client.getGenerativeModel({
+ model: this.model,
+ generationConfig: {
+ temperature: this.config.temperature,
+ maxOutputTokens: this.config.maxTokens,
+ topP: this.config.topP,
+ },
+ });
+
+ // Use chat API to send messages and get streaming response
+ const chat = generativeModel.startChat();
+ const streamingResponse = await chat.sendMessageStream(
+ formattedMessages[formattedMessages.length - 1].parts[0].text,
+ );
+
+ for await (const chunk of streamingResponse.stream) {
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ const textChunk = chunk.text();
+ if (textChunk) {
+ yield textChunk;
+ }
+ }
+ } catch (error: any) {
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ throw new Error('Request aborted');
+ }
+ throw error;
+ } finally {
+ this.cleanupRequest(requestId);
+ }
+ }
+}
diff --git a/apps/agent-tars/src/main/llmProvider/providers/MistralProvider.ts b/apps/agent-tars/src/main/llmProvider/providers/MistralProvider.ts
new file mode 100644
index 000000000..8b54c977c
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/providers/MistralProvider.ts
@@ -0,0 +1,235 @@
+import { Message, ToolCall } from '@agent-infra/shared';
+import {
+ ChatCompletionMessageParam,
+ ChatCompletionTool,
+} from 'openai/resources';
+import { Mistral } from '@mistralai/mistralai';
+import { BaseProvider } from './BaseProvider';
+import { LLMConfig, LLMResponse, ToolChoice } from '../interfaces/LLMProvider';
+import { ChatCompletionResponse } from '@mistralai/mistralai/models/components';
+
+/**
+ * Mistral provider implementation
+ */
+export class MistralProvider extends BaseProvider {
+ private client: Mistral;
+ private model: string;
+
+ constructor(config: LLMConfig = {}) {
+ super(config);
+
+ // Use environment variables or defaults
+ const apiKey = config.apiKey || process.env.MISTRAL_API_KEY;
+
+ if (!apiKey) {
+ throw new Error(
+ 'Mistral API key is required. Set MISTRAL_API_KEY environment variable or provide in config.',
+ );
+ }
+
+ // Initialize client with the actual SDK
+ this.client = new Mistral({
+ apiKey,
+ });
+
+ // Set default model or use provided one
+ this.model =
+ config.model ||
+ process.env.MISTRAL_DEFAULT_MODEL ||
+ 'mistral-large-latest';
+ }
+
+ /**
+ * Convert Message objects to Mistral API format
+ */
+ protected formatMessages(messages: Message[]): ChatCompletionMessageParam[] {
+ return messages.map((message) => ({
+ role: message.role,
+ content: message.content,
+ })) as ChatCompletionMessageParam[];
+ }
+
+ /**
+ * Process tool calls from Mistral response
+ */
+ private processToolCalls(
+ response: ChatCompletionResponse,
+ ): ToolCall[] | undefined {
+ if (!response.choices) {
+ return undefined;
+ }
+ const toolCalls = response.choices[0]?.message?.toolCalls;
+ if (!toolCalls || toolCalls.length === 0) {
+ return undefined;
+ }
+
+ return toolCalls.map((call) => ({
+ id: call.id || `mistral-tool-call-${Date.now()}`,
+ type: 'function' as const,
+ function: {
+ name: call.function.name,
+ arguments: (call.function.arguments || {}) as any,
+ },
+ }));
+ }
+
+ /**
+ * Send a message to the LLM and get a response
+ */
+ async askLLMText({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ // Use the Mistral SDK to send the request
+ const response = await this.client.chat.complete({
+ model: this.model,
+ messages: formattedMessages as any,
+ temperature: this.config.temperature || 0,
+ maxTokens: this.config.maxTokens,
+ topP: this.config.topP,
+ });
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ this.cleanupRequest(requestId);
+ const messageContent = response.choices?.[0].message.content || '';
+ if (Array.isArray(messageContent)) {
+ return messageContent
+ .map((c) => (c.type === 'text' ? c.text : ''))
+ .join('');
+ }
+ return messageContent;
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return '';
+ }
+ throw new Error(`Failed to get response from Mistral: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM with tools and get a response with potential tool calls
+ */
+ async askTool({
+ messages,
+ tools,
+ requestId,
+ }: {
+ messages: Message[];
+ tools: ChatCompletionTool[];
+ requestId: string;
+ toolChoice?: ToolChoice;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ // Convert OpenAI style tools to Mistral format
+ const mistralTools = tools.map((tool) => ({
+ type: 'function' as const,
+ function: {
+ name: tool.function.name,
+ description: tool.function.description || '',
+ parameters: tool.function.parameters || ({} as Record),
+ },
+ }));
+
+ // Send the request with tools
+ const response = await this.client.chat.complete({
+ model: this.model,
+ messages: formattedMessages as any,
+ temperature: this.config.temperature || 0,
+ maxTokens: this.config.maxTokens,
+ topP: this.config.topP,
+ tools: mistralTools,
+ toolChoice: 'required',
+ });
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ this.cleanupRequest(requestId);
+ const messageContent = response.choices?.[0].message.content || '';
+ let content = '';
+ if (Array.isArray(messageContent)) {
+ content = messageContent
+ .map((c) => (c.type === 'text' ? c.text : ''))
+ .join('');
+ }
+ const toolCalls = this.processToolCalls(response);
+ return {
+ content,
+ tool_calls: toolCalls,
+ };
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return { content: '' };
+ }
+ throw new Error(`Failed to get tool response from Mistral: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM and get a streaming response
+ */
+ async *askLLMTextStream({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): AsyncGenerator {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ // Use Mistral streaming API
+ const stream = await this.client.chat.stream({
+ model: this.model,
+ messages: formattedMessages as any,
+ temperature: this.config.temperature || 0,
+ maxTokens: this.config.maxTokens,
+ topP: this.config.topP,
+ });
+
+ for await (const chunk of stream) {
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ const content = chunk.data.choices?.[0]?.delta.content;
+ let textContent = content;
+ if (Array.isArray(textContent)) {
+ textContent = textContent
+ .map((c) => (c.type === 'text' ? c.text : ''))
+ .join('');
+ }
+ if (textContent) {
+ yield textContent;
+ }
+ }
+ } catch (error: any) {
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ throw new Error('Request aborted');
+ }
+ throw error;
+ } finally {
+ this.cleanupRequest(requestId);
+ }
+ }
+}
diff --git a/apps/agent-tars/src/main/llmProvider/providers/OpenAIProvider.ts b/apps/agent-tars/src/main/llmProvider/providers/OpenAIProvider.ts
new file mode 100644
index 000000000..1f7974f50
--- /dev/null
+++ b/apps/agent-tars/src/main/llmProvider/providers/OpenAIProvider.ts
@@ -0,0 +1,223 @@
+import OpenAI from 'openai';
+import { Message, ToolCall } from '@agent-infra/shared';
+import { ChatCompletionTool } from 'openai/resources';
+import { BaseProvider } from './BaseProvider';
+import { LLMConfig, LLMResponse, ToolChoice } from '../interfaces/LLMProvider';
+
+/**
+ * OpenAI provider implementation
+ */
+export class OpenAIProvider extends BaseProvider {
+ private client: OpenAI;
+ private model: string;
+
+ constructor(config: LLMConfig = {}) {
+ super(config);
+
+ // Use environment variables or defaults
+ const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
+ const baseURL = config.baseURL || process.env.OPENAI_API_BASE_URL;
+
+ if (!apiKey) {
+ throw new Error(
+ 'OpenAI API key is required. Set OPENAI_API_KEY environment variable or provide in config.',
+ );
+ }
+
+ this.client = new OpenAI({ apiKey, baseURL });
+ this.model = config.model || process.env.OPENAI_DEFAULT_MODEL || 'gpt-4o';
+ }
+
+ /**
+ * Convert Message objects to OpenAI API format
+ */
+ protected formatMessages(
+ messages: Message[],
+ ): OpenAI.Chat.ChatCompletionMessageParam[] {
+ return messages.map((item) => ({
+ role: item.role as any,
+ content: item.content,
+ ...(item.tool_call_id && { tool_call_id: item.tool_call_id }),
+ ...((item.tool_calls && { tool_calls: item.tool_calls }) as any),
+ ...(item.name && { name: item.name }),
+ }));
+ }
+
+ /**
+ * Process tool calls from response
+ */
+ private processToolCalls(
+ response: OpenAI.Chat.ChatCompletion,
+ ): ToolCall[] | undefined {
+ const toolCalls = response.choices
+ .filter((choice) => choice.finish_reason === 'tool_calls')
+ .map((choice) => choice.message.tool_calls)
+ .flat();
+
+ if (!toolCalls || toolCalls.length === 0) return undefined;
+
+ return toolCalls
+ .map((toolCall) => {
+ if (!toolCall) {
+ return undefined;
+ }
+
+ return {
+ id: toolCall.id,
+ type: 'function' as const,
+ function: {
+ name: toolCall.function.name,
+ arguments: toolCall.function.arguments,
+ },
+ };
+ })
+ .filter(Boolean) as ToolCall[];
+ }
+
+ /**
+ * Send a message to the LLM and get a response
+ */
+ async askLLMText({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ const response = await this.client.chat.completions.create(
+ {
+ model: this.model,
+ messages: formattedMessages,
+ temperature: this.config.temperature,
+ max_tokens: this.config.maxTokens,
+ top_p: this.config.topP,
+ frequency_penalty: this.config.frequencyPenalty,
+ presence_penalty: this.config.presencePenalty,
+ },
+ {
+ signal: controller.signal,
+ },
+ );
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+ this.cleanupRequest(requestId);
+ return response.choices[0].message.content || '';
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return '';
+ }
+ throw new Error(`Failed to get response from OpenAI: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM with tools and get a response with potential tool calls
+ */
+ async askTool({
+ messages,
+ tools,
+ requestId,
+ toolChoice = 'auto',
+ }: {
+ messages: Message[];
+ tools: ChatCompletionTool[];
+ requestId: string;
+ toolChoice?: ToolChoice;
+ }): Promise {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ const response = await this.client.chat.completions.create(
+ {
+ model: this.model,
+ messages: formattedMessages,
+ temperature: this.config.temperature,
+ max_tokens: this.config.maxTokens,
+ tools,
+ tool_choice: toolChoice,
+ top_p: this.config.topP,
+ frequency_penalty: this.config.frequencyPenalty,
+ presence_penalty: this.config.presencePenalty,
+ },
+ {
+ signal: controller.signal,
+ },
+ );
+
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+
+ this.cleanupRequest(requestId);
+ const content = response.choices[0].message.content;
+ const toolCalls = this.processToolCalls(response);
+ return { content, tool_calls: toolCalls };
+ } catch (error: any) {
+ this.cleanupRequest(requestId);
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ return { content: '' };
+ }
+ throw new Error(`Failed to get tool response from OpenAI: ${error}`);
+ }
+ }
+
+ /**
+ * Send a message to the LLM and get a streaming response
+ */
+ async *askLLMTextStream({
+ messages,
+ requestId,
+ }: {
+ messages: Message[];
+ requestId: string;
+ }): AsyncGenerator {
+ try {
+ const formattedMessages = this.formatMessages(messages);
+ const controller = new AbortController();
+ this.activeRequests.set(requestId, controller);
+
+ const stream = await this.client.chat.completions.create(
+ {
+ model: this.model,
+ messages: formattedMessages,
+ temperature: this.config.temperature,
+ max_tokens: this.config.maxTokens,
+ top_p: this.config.topP,
+ frequency_penalty: this.config.frequencyPenalty,
+ presence_penalty: this.config.presencePenalty,
+ stream: true,
+ },
+ {
+ signal: controller.signal,
+ },
+ );
+
+ for await (const chunk of stream) {
+ if (controller.signal.aborted) {
+ throw new Error('Request aborted');
+ }
+ const content = chunk.choices[0]?.delta?.content;
+ if (content) {
+ yield content;
+ }
+ }
+ } catch (error: any) {
+ if (error.name === 'AbortError' || error.message.includes('aborted')) {
+ throw new Error('Request aborted');
+ }
+ throw error;
+ } finally {
+ this.cleanupRequest(requestId);
+ }
+ }
+}
diff --git a/apps/agent-tars/src/main/mcp/client.ts b/apps/agent-tars/src/main/mcp/client.ts
new file mode 100644
index 000000000..6f05d438b
--- /dev/null
+++ b/apps/agent-tars/src/main/mcp/client.ts
@@ -0,0 +1,84 @@
+import { MCPClient } from '@agent-infra/mcp-client';
+import { MCPServerName } from '@agent-infra/shared';
+import path from 'path';
+import os from 'os';
+import fs from 'fs-extra';
+
+// Keep track of the filesystem client to allow updating allowed directories
+let fsClientModule: any = null;
+
+export const getOmegaDir = async () => {
+ // Create working directory in user's home directory.
+ const omegaDir = path.join(os.homedir(), '.omega');
+ if (!fs.existsSync(omegaDir)) {
+ await fs.mkdir(omegaDir, { recursive: true });
+ }
+ return omegaDir;
+};
+
+const dynamicImport = (url) =>
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
+ new Function(`return import('${url}')`)();
+
+// Initialize MCP client with filesystem and commands tools
+export const createMcpClient = async () => {
+ if (mapClientRef.current) {
+ return mapClientRef.current;
+ }
+ const commandModule = await dynamicImport('@agent-infra/mcp-server-commands');
+ const fsModule = await dynamicImport('@agent-infra/mcp-server-filesystem');
+ const browserModule = await dynamicImport('@agent-infra/mcp-server-browser');
+
+ const { client: commandClient } = commandModule.default;
+ const { client: fsClient, setAllowedDirectories } = fsModule.default;
+ const { client: browserClient } = browserModule.default;
+
+ fsClientModule = fsModule.default;
+
+ const omegaDir = await getOmegaDir();
+ setAllowedDirectories([omegaDir]);
+
+ const toolsMap = {
+ [MCPServerName.FileSystem]: {
+ name: MCPServerName.FileSystem,
+ description: 'filesystem tool',
+ localClient: fsClient,
+ },
+ [MCPServerName.Commands]: {
+ name: MCPServerName.Commands,
+ description: 'commands tool',
+ localClient: commandClient,
+ },
+ [MCPServerName.Browser]: {
+ name: MCPServerName.Browser,
+ local: true,
+ description: 'browser tools',
+ localClient: browserClient,
+ },
+ };
+
+ const client = new MCPClient(Object.values(toolsMap));
+ mapClientRef.current = client;
+ return client;
+};
+
+export const mapClientRef: {
+ current: MCPClient | undefined;
+} = {
+ current: undefined,
+};
+
+export const setAllowedDirectories = async (directories: string[]) => {
+ if (fsClientModule && fsClientModule.setAllowedDirectories) {
+ return fsClientModule.setAllowedDirectories(directories);
+ }
+ throw new Error('File system client not initialized');
+};
+
+export const getAllowedDirectories = async (): Promise => {
+ if (fsClientModule && fsClientModule.getAllowedDirectories) {
+ return fsClientModule.getAllowedDirectories();
+ }
+ const omegaDir = await getOmegaDir();
+ return [omegaDir];
+};
diff --git a/apps/agent-tars/src/main/mcp/tools.ts b/apps/agent-tars/src/main/mcp/tools.ts
new file mode 100644
index 000000000..748405116
--- /dev/null
+++ b/apps/agent-tars/src/main/mcp/tools.ts
@@ -0,0 +1,30 @@
+import { MCPServerName } from '@agent-infra/shared';
+import { MCPTool } from '@agent-infra/mcp-client';
+import { ChatCompletionTool } from 'openai/resources/index.mjs';
+
+export function mcpToolsToAzureTools(
+ mcpTools: MCPTool[],
+): Array {
+ return mcpTools.map((tool) => {
+ const t = {
+ type: 'function' as const,
+ function: {
+ name: tool.name,
+ description: tool.description,
+ parameters: tool.inputSchema,
+ },
+ };
+ return t;
+ });
+}
+
+export function mapToolKeysToAzureTools(
+ mcpTools: MCPTool[],
+ toolKeys: MCPServerName[],
+) {
+ return mcpToolsToAzureTools(
+ mcpTools.filter((tool) =>
+ toolKeys.includes(tool.serverName as MCPServerName),
+ ),
+ );
+}
diff --git a/apps/agent-tars/src/main/type.ts b/apps/agent-tars/src/main/type.ts
new file mode 100644
index 000000000..f66d086d7
--- /dev/null
+++ b/apps/agent-tars/src/main/type.ts
@@ -0,0 +1,4 @@
+import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
+import { z } from 'zod';
+
+export type MCPToolResult = z.infer[];
diff --git a/apps/agent-tars/src/main/utils/normalizeOmegaData.ts b/apps/agent-tars/src/main/utils/normalizeOmegaData.ts
new file mode 100644
index 000000000..b16083f87
--- /dev/null
+++ b/apps/agent-tars/src/main/utils/normalizeOmegaData.ts
@@ -0,0 +1,144 @@
+import { readFile } from 'fs-extra';
+import path from 'path';
+
+interface ImageItem {
+ type: 'image';
+ path: string;
+ content?: string;
+}
+
+interface ToolUsedEvent {
+ type: 'tool-used';
+ content: {
+ result: Array;
+ };
+}
+
+interface OmegaAgentMessage {
+ type: 'omega-agent';
+ content: {
+ events: Array;
+ };
+}
+
+function isImage(filePath: string): boolean {
+ const ext = path.extname(filePath).toLowerCase();
+ return ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(ext);
+}
+
+export async function normalizeMessages(messages: Array) {
+ const normalizedMessages = await Promise.all(
+ messages.map(async (item) => {
+ if (item.type !== 'omega-agent') {
+ return item;
+ }
+
+ const normalizedEvents = await Promise.all(
+ item.content.events.map(async (event) => {
+ if (
+ event.type === 'tool-used' &&
+ event.content.tool.includes('browser')
+ ) {
+ const result = event.content.result || [];
+ const normalizedResults = await Promise.all(
+ result.map(async (resultItem) => {
+ if (resultItem.type === 'image') {
+ // base64 image
+ if (
+ !resultItem.path &&
+ resultItem.data &&
+ resultItem.mimeType
+ ) {
+ return {
+ ...resultItem,
+ content: `data:${resultItem.mimeType};base64,${resultItem.data}`,
+ };
+ }
+
+ // binary png image
+ const base64Content = await readFile(
+ resultItem.path,
+ 'base64',
+ );
+ delete resultItem.path;
+ return {
+ ...resultItem,
+ content: `data:image/png;base64,${base64Content}`,
+ };
+ }
+ return resultItem;
+ }),
+ );
+ return {
+ ...event,
+ content: {
+ ...event.content,
+ result: normalizedResults,
+ },
+ };
+ }
+ return event;
+ }),
+ );
+
+ return {
+ ...item,
+ content: {
+ ...item.content,
+ events: normalizedEvents,
+ },
+ };
+ }),
+ );
+
+ return normalizedMessages;
+}
+
+export async function parseArtifacts(messages: Array) {
+ let artifacts: {
+ [key: string]: {
+ content: string;
+ };
+ } = {};
+
+ await Promise.all(
+ messages.map(async (item) => {
+ if (item.type !== 'omega-agent') {
+ return;
+ }
+
+ await Promise.all(
+ item.content.events.map(async (event) => {
+ if (event.type === 'chat-text') {
+ const { attachments = [] } = event.content;
+ await Promise.all(
+ attachments.map(async (attachment) => {
+ const artifactPath = attachment.path;
+ const fileName = path.basename(artifactPath);
+
+ try {
+ if (isImage(artifactPath)) {
+ const base64Content = await readFile(
+ artifactPath,
+ 'base64',
+ );
+ artifacts[fileName] = {
+ content: `data:image/${path.extname(artifactPath).slice(1)};base64,${base64Content}`,
+ };
+ } else {
+ const content = await readFile(artifactPath, 'utf-8');
+ artifacts[fileName] = { content };
+ }
+ } catch (error) {
+ console.error(`Failed to read file: ${artifactPath}`, error);
+ }
+ }),
+ );
+ }
+ }),
+ );
+ }),
+ );
+
+ return artifacts;
+}
diff --git a/apps/agent-tars/src/preload/index.d.ts b/apps/agent-tars/src/preload/index.d.ts
new file mode 100644
index 000000000..0cc56585a
--- /dev/null
+++ b/apps/agent-tars/src/preload/index.d.ts
@@ -0,0 +1,11 @@
+import { ElectronAPI } from '@electron-toolkit/preload';
+
+declare global {
+ interface Window {
+ electron: ElectronAPI;
+ api: {
+ on: (channel: string, callback: (...args: any[]) => void) => void;
+ off: (channel: string, callback: (...args: any[]) => void) => void;
+ };
+ }
+}
diff --git a/apps/agent-tars/src/preload/index.ts b/apps/agent-tars/src/preload/index.ts
new file mode 100644
index 000000000..2a9f920a3
--- /dev/null
+++ b/apps/agent-tars/src/preload/index.ts
@@ -0,0 +1,26 @@
+import { contextBridge, ipcRenderer } from 'electron';
+import { electronAPI } from '@electron-toolkit/preload';
+
+// Custom APIs for renderer
+const api = {
+ on: (channel: string, callback: (...args: any[]) => void) => {
+ ipcRenderer.on(channel, (_event, ...args) => callback(...args));
+ },
+ off: (channel: string, callback: (...args: any[]) => void) => {
+ ipcRenderer.removeListener(channel, callback);
+ },
+};
+
+if (process.contextIsolated) {
+ try {
+ contextBridge.exposeInMainWorld('electron', electronAPI);
+ contextBridge.exposeInMainWorld('api', api);
+ } catch (error) {
+ console.error(error);
+ }
+} else {
+ // @ts-ignore (define in dts)
+ window.electron = electronAPI;
+ // @ts-ignore (define in dts)
+ window.api = api;
+}
diff --git a/apps/agent-tars/src/renderer/index.html b/apps/agent-tars/src/renderer/index.html
new file mode 100644
index 000000000..fc57ff9e0
--- /dev/null
+++ b/apps/agent-tars/src/renderer/index.html
@@ -0,0 +1,44 @@
+
+
+
+
+ Agent TARS
+
+
+
+
+
+
+
+
+
diff --git a/apps/agent-tars/src/renderer/mock/ipc.ts b/apps/agent-tars/src/renderer/mock/ipc.ts
new file mode 100644
index 000000000..6ccd08623
--- /dev/null
+++ b/apps/agent-tars/src/renderer/mock/ipc.ts
@@ -0,0 +1,131 @@
+import { MCPToolResult } from '../../main/type';
+
+const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+// Mock tool result for web_search
+const mockSearchResult: MCPToolResult = [
+ {
+ isError: false,
+ content: [
+ {
+ title: 'Mock Search Result 1',
+ url: 'https://example.com/1',
+ snippet: 'This is a mock search result.',
+ },
+ {
+ title: 'Mock Search Result 2',
+ url: 'https://example.com/2',
+ snippet: 'Another mock search result.',
+ },
+ ],
+ },
+];
+
+// Mock tools list
+const mockTools = [
+ {
+ name: 'web_search',
+ description: 'Search in the internet',
+ parameters: {
+ type: 'object',
+ properties: {
+ query: {
+ type: 'string',
+ description: 'The search query',
+ },
+ },
+ required: ['query'],
+ },
+ },
+];
+
+export const createClient = () => ({
+ askLLMText: async () => {
+ await delay(1000);
+ return "I'm a mock response from the LLM.";
+ },
+
+ askLLMTool: async () => {
+ await delay(1000);
+ return {
+ content: null,
+ tool_calls: [
+ {
+ id: 'mock-tool-call',
+ type: 'function',
+ function: {
+ name: 'web_search',
+ arguments: JSON.stringify({
+ query: 'mock search query',
+ }),
+ },
+ },
+ ],
+ };
+ },
+
+ askLLMTextStream: async ({ requestId }: { requestId: string }) => {
+ return requestId;
+ },
+
+ abortRequest: async ({ requestId }: { requestId: string }) => {
+ return true;
+ },
+
+ listTools: async () => {
+ await delay(500);
+ return mockTools;
+ },
+
+ listMcpTools: async () => {
+ await delay(500);
+ return [];
+ },
+
+ listCustomTools: async () => {
+ await delay(500);
+ return mockTools;
+ },
+
+ executeTool: async () => {
+ await delay(1000);
+ return mockSearchResult;
+ },
+
+ saveBrowserSnapshot: async () => {
+ await delay(500);
+ return {
+ filepath: '/mock/path/to/screenshot.png',
+ };
+ },
+
+ saveReportHtml: async () => {
+ await delay(500);
+ return '/mock/path/to/report.html';
+ },
+
+ cleanup: async () => {
+ return true;
+ },
+
+ runAgent: async () => {
+ return 'Hello from mock agent';
+ },
+});
+
+declare global {
+ interface Window {
+ electron: {
+ ipcRenderer: {
+ invoke: (channel: string, ...args: any[]) => Promise;
+ };
+ };
+ }
+}
+window.electron = {
+ ipcRenderer: {
+ invoke: async () => {
+ // noop
+ },
+ },
+};
diff --git a/apps/agent-tars/src/renderer/package.json b/apps/agent-tars/src/renderer/package.json
new file mode 100644
index 000000000..b9e3d91dc
--- /dev/null
+++ b/apps/agent-tars/src/renderer/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "open-agent-renderer",
+ "dependencies": {
+ "@ui-tars/electron-ipc": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "@nextui-org/react": "2.4.8",
+ "framer-motion": "11.2.13",
+ "styled-components": "6.1.11",
+ "localforage": "^1.10.0",
+ "jotai": "^2.8.2",
+ "react-icons": "5.2.1",
+ "lucide-react": "0.479.0",
+ "classnames": "2.5.1",
+ "jszip": "3.10.1",
+ "@nextui-org/system": "2.2.6",
+ "@nextui-org/theme": "2.2.11",
+ "@agent-infra/shared": "workspace:*",
+ "clsx": "2.1.1",
+ "tailwind-merge": "3.0.2",
+ "use-dark-mode": "2.3.1",
+ "eventemitter3": "5.0.1",
+ "dayjs": "1.11.13",
+ "date-fns": "4.1.0",
+ "react-split": "2.0.14",
+ "@monaco-editor/react": "4.7.0",
+ "xterm": "^5.3.0",
+ "xterm-addon-fit": "^0.8.0",
+ "xterm-addon-web-links": "^0.9.0",
+ "jsonrepair": "3.12.0",
+ "react-markdown": "10.1.0",
+ "remark-gfm": "4.0.1",
+ "remark-math": "6.0.0",
+ "rehype-katex": "7.0.1",
+ "lodash-es": "4.17.21",
+ "katex": "0.16.21",
+ "react-syntax-highlighter": "15.5.0",
+ "uuid": "11.1.0",
+ "react-hot-toast": "2.5.2",
+ "path-browserify": "1.0.1"
+ },
+ "devDependencies": {
+ "@modelcontextprotocol/sdk": "^1.7.0",
+ "typescript": "^5.7.3",
+ "@types/react": "^18.3.18",
+ "@types/react-dom": "^18.3.5",
+ "@tailwindcss/aspect-ratio": "0.4.2",
+ "@agent-infra/search": "workspace:*"
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/App.tsx b/apps/agent-tars/src/renderer/src/App.tsx
new file mode 100644
index 000000000..035d9c8a1
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/App.tsx
@@ -0,0 +1,20 @@
+// import { useState } from 'react';
+// import { api } from '@renderer/api';
+import { AgentApp } from './components/AgentApp';
+import './index.scss';
+
+function App(): JSX.Element {
+ // const [agentResponse, setAgentResponse] = useState();
+ // const callAgent = async (): Promise => {
+ // const res = await api.runAgent();
+ // setAgentResponse(res);
+ // };
+
+ return (
+
+ );
+}
+
+export default App;
diff --git a/apps/agent-tars/src/renderer/src/agent/AgentFlow.ts b/apps/agent-tars/src/renderer/src/agent/AgentFlow.ts
new file mode 100644
index 000000000..2ab5df01e
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/agent/AgentFlow.ts
@@ -0,0 +1,444 @@
+import { Memory } from '@agent-infra/shared';
+import { ChatMessageUtil } from '@renderer/utils/ChatMessageUtils';
+import { AppContext } from '@renderer/hooks/useAgentFlow';
+import { Aware, AwareResult } from './Aware';
+import { Executor } from './Executor';
+import {
+ PlanTask,
+ PlanTaskStatus,
+ ToolCallParam,
+ ToolCallType,
+} from '@renderer/type/agent';
+import { EventManager } from './EventManager';
+import { ExecutorToolType } from './Executor/tools';
+import { ipcClient } from '@renderer/api';
+import { GlobalEvent, globalEventEmitter } from '@renderer/state/chat';
+import { Greeter } from './Greeter';
+import { extractHistoryEvents } from '@renderer/utils/extractHistoryEvents';
+import { EventItem, EventType } from '@renderer/type/event';
+import { SNAPSHOT_BROWSER_ACTIONS } from '@renderer/constants';
+import { loadLLMSettings } from '@renderer/services/llmSettings';
+
+export interface AgentContext {
+ plan: PlanTask[];
+ currentStep: number;
+ memory: Memory;
+ getEnvironmentInfo: (
+ appContext: AppContext,
+ agentContext: AgentContext,
+ ) => string;
+ eventManager: EventManager;
+}
+
+export interface EventStream {}
+
+export class AgentFlow {
+ private eventManager: EventManager;
+ private abortController: AbortController;
+ private interruptController: AbortController;
+ private hasFinished = false;
+ private loadingStatusTip = '';
+
+ constructor(private appContext: AppContext) {
+ // Load LLM settings and update the configuration
+ const llmSettings = loadLLMSettings();
+ if (llmSettings) {
+ // Update LLM configuration when starting the agent flow
+ ipcClient
+ .updateLLMConfig({
+ configName: llmSettings.provider,
+ model: llmSettings.model,
+ apiKey: llmSettings.apiKey,
+ apiVersion: llmSettings.apiVersion,
+ baseURL: llmSettings.endpoint,
+ })
+ .catch((error) => {
+ console.error('Failed to update LLM configuration:', error);
+ });
+ }
+
+ const omegaHistoryEvents = this.parseHistoryEvents();
+ this.eventManager = new EventManager(omegaHistoryEvents);
+ this.abortController = new AbortController();
+ this.interruptController = new AbortController();
+ }
+
+ async run() {
+ this.appContext.setPlanTasks([]);
+ const chatUtils = this.appContext.chatUtils;
+ const { setAgentStatusTip } = this.appContext;
+ this.eventManager.addLoadingStatus('Thinking');
+ chatUtils.addMessage(
+ ChatMessageUtil.assistantOmegaMessage({
+ events: this.eventManager.getAllEvents(),
+ }),
+ {
+ shouldSyncStorage: true,
+ },
+ );
+ // this.appContext.setEvents([
+ // ...this.eventManager.getHistoryEvents(),
+ // ...this.eventManager.getAllEvents(),
+ // ]);
+ // return;
+ setAgentStatusTip('Thinking');
+ // First, we give a loading ui to the user to indicate that the agent is thinking
+ const agentContext: AgentContext = {
+ plan: [],
+ currentStep: 0,
+ memory: new Memory(),
+ getEnvironmentInfo: this.getEnvironmentInfo,
+ eventManager: this.eventManager,
+ };
+ const aware = new Aware(
+ this.appContext,
+ agentContext,
+ this.interruptController.signal,
+ );
+ const executor = new Executor(
+ this.appContext,
+ agentContext,
+ this.interruptController.signal,
+ );
+ this.eventManager.addLoadingStatus('Thinking');
+ const greeter = new Greeter(this.appContext, this.abortController.signal);
+
+ globalEventEmitter.addListener(
+ this.appContext.agentFlowId,
+ async (event) => {
+ if (event.type === 'terminate') {
+ this.abortController.abort();
+ await this.eventManager.addEndEvent(
+ 'Agent flow has been terminated.',
+ );
+ }
+ },
+ );
+ const preparePromise = greeter.run().then(async () => {
+ // Run planner agent
+ const omegaMessage = await chatUtils.addMessage(
+ ChatMessageUtil.assistantOmegaMessage({
+ events: this.eventManager.getAllEvents(),
+ }),
+ {
+ shouldSyncStorage: true,
+ },
+ );
+ // Bind event data and ui, when event added, the ui will be updated automatically
+ // In other words, reactive ui programming
+ this.eventManager.setUpdateCallback(async (events) => {
+ this.appContext.setEvents((preEvents: EventItem[]) => {
+ // Show canvas automatically when tool used
+ if (preEvents.find((e) => e.type === EventType.ToolUsed)) {
+ this.appContext.setShowCanvas(true);
+ }
+ const latestToolUsedEvent = [...events]
+ .reverse()
+ .find((e) => e.type === EventType.ToolUsed);
+ console.log('latestToolUsedEvent', latestToolUsedEvent);
+ latestToolUsedEvent &&
+ this.appContext.setEventId(latestToolUsedEvent.id);
+ return [...this.eventManager.getHistoryEvents(), ...events];
+ });
+ await chatUtils.updateMessage(
+ ChatMessageUtil.assistantOmegaMessage({ events }),
+ {
+ messageId: omegaMessage!.id,
+ shouldSyncStorage: true,
+ shouldScrollToBottom: true,
+ },
+ );
+ });
+
+ globalEventEmitter.addListener(
+ this.appContext.agentFlowId,
+ async (event: GlobalEvent) => {
+ switch (event.type) {
+ case 'user-interrupt':
+ await this.eventManager.addUserInteruptionInput(event.text);
+ this.interruptController.abort();
+ await chatUtils.updateMessage(
+ ChatMessageUtil.assistantOmegaMessage({
+ events: this.eventManager.getAllEvents(),
+ }),
+ {
+ messageId: omegaMessage!.id,
+ shouldSyncStorage: true,
+ },
+ );
+ break;
+ default:
+ break;
+ }
+ },
+ );
+ });
+
+ await Promise.all([
+ preparePromise,
+ this.launchAgentLoop(executor, aware, agentContext, preparePromise),
+ ]);
+
+ if (!this.abortController.signal.aborted) {
+ this.eventManager.addEndEvent('> Agent TARS has finished.');
+ }
+ }
+
+ private async launchAgentLoop(
+ executor: Executor,
+ aware: Aware,
+ agentContext: AgentContext,
+ preparePromise: Promise,
+ ) {
+ this.loadingStatusTip = 'Thinking';
+ try {
+ while (!this.abortController.signal.aborted && !this.hasFinished) {
+ // The environment includes
+ // - The current event stream context
+ // - The current task
+ // - The current user message
+ try {
+ await this.eventManager.addLoadingStatus(this.loadingStatusTip);
+ console.log(
+ 'env info',
+ this.getEnvironmentInfo(this.appContext, agentContext),
+ );
+ const awareResult = await aware.run();
+ this.loadingStatusTip = 'Thinking';
+ await preparePromise;
+ if (this.abortController.signal.aborted) {
+ break;
+ }
+ if (
+ awareResult.plan &&
+ awareResult.plan.every(
+ (task) => task.status === PlanTaskStatus.Done,
+ )
+ ) {
+ this.hasFinished = true;
+ break;
+ }
+
+ if (this.interruptController.signal.aborted) {
+ this.interruptController = new AbortController();
+ this.loadingStatusTip = 'Replaning';
+ aware.updateSignal(this.interruptController.signal);
+ executor.updateSignal(this.interruptController.signal);
+ await this.eventManager.addLoadingStatus(this.loadingStatusTip);
+ this.appContext.setAgentStatusTip(this.loadingStatusTip);
+ continue;
+ }
+ console.log('aware result', awareResult);
+
+ // Reset the plan
+ agentContext.plan = this.normalizePlan(awareResult, agentContext);
+ await this.eventManager.addPlanUpdate(
+ awareResult.step,
+ agentContext.plan,
+ );
+ this.appContext.setPlanTasks(agentContext.plan);
+ if (agentContext.plan.length === 0) {
+ this.hasFinished = true;
+ break;
+ }
+ agentContext.currentStep = awareResult.step;
+ if (awareResult.step > agentContext.currentStep) {
+ // Update UI, render new step
+ await this.eventManager.addNewPlanStep(agentContext.currentStep);
+ // Over the max task number, break the loop
+ if (awareResult.step > agentContext.plan.length) {
+ break;
+ }
+ }
+ if (awareResult.status) {
+ // Update UI, render new status
+ await this.eventManager.addAgentStatus(awareResult.status);
+ }
+
+ await this.eventManager.addLoadingStatus(this.loadingStatusTip);
+ this.appContext.setAgentStatusTip(this.loadingStatusTip);
+
+ const toolCallList = (await executor.run(awareResult.status)).filter(
+ Boolean,
+ );
+
+ if (this.abortController.signal.aborted) {
+ break;
+ }
+
+ if (this.interruptController.signal.aborted) {
+ this.handleUserInterrupt(aware, executor);
+ continue;
+ }
+
+ const mcpTools = await ipcClient.listMcpTools();
+ const customServerTools = await ipcClient.listCustomTools();
+ this.loadingStatusTip = 'Executing Tool';
+ // Execute the tools
+ for (const toolCall of toolCallList) {
+ const toolName = toolCall.function.name;
+ const isMCPToolCall = mcpTools.some(
+ (tool) => tool.name === toolCall.function.name,
+ );
+ const isCustomServerToolCall = customServerTools.some(
+ (tool) => tool.function.name === toolCall.function.name,
+ );
+
+ await this.eventManager.addToolCallStart(
+ toolName,
+ toolCall.function.arguments,
+ );
+
+ await this.eventManager.addToolExecutionLoading(toolCall);
+
+ // Set up permission check interval for this specific tool execution
+ let originalFileContent: string | null = null;
+
+ if (isMCPToolCall || isCustomServerToolCall) {
+ if (
+ toolName === ToolCallType.EditFile ||
+ toolName === ToolCallType.WriteFile
+ ) {
+ const params = JSON.parse(
+ toolCall.function.arguments,
+ ) as ToolCallParam['edit_file'];
+ originalFileContent = await ipcClient.getFileContent({
+ filePath: params.path,
+ });
+ }
+ // Execute tool in the main thread
+ const callResult = (await executor.excuteTools([toolCall]))[0];
+ this.appContext.setAgentStatusTip('Executing Tool');
+
+ await this.eventManager.handleToolExecution({
+ toolName,
+ toolCallId: toolCall.id,
+ params: toolCall.function.arguments,
+ result: callResult.content,
+ isError: callResult.isError as boolean,
+ });
+ }
+
+ if (originalFileContent) {
+ // Add the missing original file content for diff code display
+ this.eventManager.updateFileContentForEdit(originalFileContent);
+ }
+
+ if (SNAPSHOT_BROWSER_ACTIONS.includes(toolName as ToolCallType)) {
+ const screenshotPath = await ipcClient.saveBrowserSnapshot();
+ console.log('screenshotPath', screenshotPath);
+ this.eventManager.updateScreenshot(screenshotPath.filepath);
+ }
+
+ if (toolName === ExecutorToolType.ChatMessage) {
+ const params = JSON.parse(toolCall.function.arguments);
+ await this.eventManager.addChatText(
+ params.text,
+ params.attachments,
+ );
+ }
+
+ if (toolName === ExecutorToolType.Idle) {
+ this.hasFinished = true;
+ this.eventManager.addPlanUpdate(
+ agentContext.plan.length,
+ this.flagPlanDone(agentContext.plan),
+ );
+ break;
+ }
+ }
+ this.loadingStatusTip = 'Thinking';
+ } catch (e) {
+ console.log(e);
+ break;
+ }
+ }
+ } catch (error) {
+ if (error instanceof DOMException && error.name === 'AbortError') {
+ console.log('Agent loop aborted');
+ return;
+ }
+ throw error;
+ }
+ }
+
+ private async handleUserInterrupt(aware: Aware, executor: Executor) {
+ this.interruptController = new AbortController();
+ aware.updateSignal(this.interruptController.signal);
+ executor.updateSignal(this.interruptController.signal);
+ this.loadingStatusTip = 'Replaning';
+ await this.eventManager.addLoadingStatus(this.loadingStatusTip);
+ this.appContext.setAgentStatusTip(this.loadingStatusTip);
+ }
+
+ private getEnvironmentInfo(
+ appContext: AppContext,
+ agentContext: AgentContext,
+ ) {
+ const pendingInit = agentContext.plan.length === 0;
+ const currentStep = agentContext.currentStep;
+ const currentTask = agentContext.plan[currentStep - 1]?.title;
+ return `Event stream result history: ${this.eventManager.normalizeEventsForPrompt()}
+
+The user original input: ${appContext.request.inputText}
+
+ ${
+ pendingInit
+ ? 'Plan: None'
+ : `Plan:
+${agentContext.plan.map((item) => ` - [${item.id}] ${item.title}`).join('\n')}
+
+Current step: ${currentStep}
+
+Current task: ${currentTask}
+`
+ }
+ `;
+ }
+
+ /**
+ * Get the event manager instance
+ */
+ public getEventManager(): EventManager {
+ return this.eventManager;
+ }
+
+ private normalizePlan(awareResult: AwareResult, agentContext: AgentContext) {
+ return (awareResult.plan || agentContext.plan).map((item, index) => {
+ // const currentTask = agentContext.plan[index];
+ if (index < awareResult.step - 1) {
+ return {
+ ...item,
+ status: PlanTaskStatus.Done,
+ };
+ }
+
+ if (index === awareResult.step - 1) {
+ return {
+ ...item,
+ status: PlanTaskStatus.Doing,
+ };
+ }
+
+ return {
+ ...item,
+ status: PlanTaskStatus.Todo,
+ };
+ });
+ }
+
+ private flagPlanDone(plan: PlanTask[]) {
+ return plan.map((item) => {
+ return {
+ ...item,
+ status: PlanTaskStatus.Done,
+ };
+ });
+ }
+
+ private parseHistoryEvents() {
+ const events = extractHistoryEvents(this.appContext.chatUtils.messages);
+ this.appContext.setEvents(events);
+ return events;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/agent/Aware.ts b/apps/agent-tars/src/renderer/src/agent/Aware.ts
new file mode 100644
index 000000000..821581a70
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/agent/Aware.ts
@@ -0,0 +1,247 @@
+import { Message } from '@agent-infra/shared';
+import { AgentContext } from './AgentFlow';
+import { ipcClient } from '@renderer/api';
+import { AppContext } from '@renderer/hooks/useAgentFlow';
+import { PlanTask } from '@renderer/type/agent';
+import { jsonrepair } from 'jsonrepair';
+
+export interface AwareResult {
+ reflection: string;
+ step: number;
+ status: string;
+ plan?: PlanTask[];
+}
+
+// Note: Aware is the `Ambient World Analysis and Response Engine` for short, meaning that it is responsible for analyzing the ambient world and response the plan for the next step.
+export class Aware {
+ constructor(
+ private appContext: AppContext,
+ private agentContext: AgentContext,
+ private abortSignal: AbortSignal,
+ ) {}
+
+ private systemPrompt = `You are an AI agent with the ability to analyze the current environment, decide the next task status, tell user the next specific action.
+
+
+You must call the aware_analysis tool.
+
+You should give the insights of current environment according to the various context information, and then decide the next task status.
+
+If the task is none or current step is done, you should increment the step number and update system status. Please return the json output in the tool call part:
+
+\`\`\`json
+{
+ "reflection": "[your reflection about current environment]",
+ "step": "[next step number]",
+ "status": "[next task description, a complete sentence tell user what to do next]",
+}
+\`\`\`
+
+You should output the reflection first.
+
+You should not output any response text and only return the tool call.
+
+Only when there is no existing plan in the current environment, you should return plan field with the following format:
+- id: string (format: "step_XXX" where XXX is a sequential number starting from 001)
+- title: string (clear, concise description of the step)
+
+
+
+
+
+For any given task or problem:
+1. Analyze the requirements thoroughly
+2. Create a systematic, step-by-step solution
+3. Ensure each step is concrete and actionable
+4. Maintain logical progression between steps
+
+
+
+You must follow these limitations:
+
+- If there is plan exist, you should not return the plan field.
+- Don't ask user anything, just tell user what to do next. If some points is not very clear, you should tell user your solution. Remember, you are a agent for human.
+- Don't output any response text and only return the tool call.
+- You should not repeat the same behavior or mean with previous steps.
+- Don't output any file path in current machine and ensure the security in your message. Don't output any absolute path in your message.
+
+
+
+
+
+Only except user interrupt or start a new session, you CANNOT update the plan!
+
+If you reset the plan to a new one, you should also reset the step to number 1.
+
+
+
+
+
+
+In the \`status\` field, you should only return a sentence to tell user what you will do next, and don't need to return the reason and other information.Please the the first person perspective to answer, indicating that you are work for the user.
+
+
+
+
+
+
+If in the last step, but we still have issues to solve, you cannot increment the step number and should continue to solve the issue.
+
+
+
+
+
+For user interrupt input in the middle of the event stream, you should handle it in the first important level and handle it as soon as possible.If current plan tasks cannot match the new user input, you should reset the plan.
+
+
+
+
+
+The event stream result record the complete response of the agent, you should make next decision base on the history, if current step has not been done, please don't increment the step number. If you meet the \`ended\` message, that means you entered a new session and you should reset the plan from stratch.
+
+In the event stream, the \`observation\` type message is the observation of the tool use, you should attention to the field and judge the task status accroding to it.When the observer represent the error message, you should reflect the error and solve it in the next step.
+
+
+
+
+
+After \`web_search\` called, then you must select web page from the search result, then you must see the detail of the page, call navigate to get detail. See the detail right away after get search result!
+
+
+
+
+
+When you want to write file, you should list allowed directories and write the file to the allowed directory.
+
+
+
+
+
+
+You should use the same language as the user input by default.
+
+
+ `;
+
+ private awareSchema = {
+ type: 'object',
+ properties: {
+ step: {
+ type: 'number',
+ description: 'Next step number',
+ },
+ status: {
+ type: 'string',
+ description:
+ 'Next task description, a complete sentence tell user what to do next',
+ },
+ reflection: {
+ type: 'string',
+ description: 'Your reflection about current environment',
+ },
+ plan: {
+ type: 'array',
+ items: {
+ type: 'object',
+ required: ['id', 'title'],
+ properties: {
+ id: {
+ type: 'string',
+ description: 'Step ID is a sequential number',
+ },
+ title: {
+ type: 'string',
+ minLength: 1,
+ description: 'Clear and concise description of the step',
+ },
+ },
+ },
+ },
+ },
+ required: ['step', 'status', 'reflection'],
+ } as const;
+
+ public updateSignal(abortSignal: AbortSignal) {
+ this.abortSignal = abortSignal;
+ }
+
+ async run() {
+ const environmentInfo = await this.agentContext.getEnvironmentInfo(
+ this.appContext,
+ this.agentContext,
+ );
+ const defaultResult = {
+ reflection: 'No plan',
+ step: this.agentContext.currentStep,
+ status: 'No plan',
+ plan: [],
+ };
+
+ if (this.abortSignal.aborted) {
+ return defaultResult;
+ }
+
+ const streamId = Math.random().toString(36).substring(7);
+ return new Promise(async (resolve, reject) => {
+ const abortHandler = () => {
+ ipcClient.abortRequest({ requestId: streamId });
+ resolve(defaultResult);
+ };
+
+ try {
+ this.abortSignal.addEventListener('abort', abortHandler);
+ const excutorTools = await ipcClient.listTools();
+ const result = await ipcClient.askLLMTool({
+ messages: [
+ Message.systemMessage(this.systemPrompt),
+ Message.systemMessage(
+ `You are working with excutor agent, here is the excutor tools: ${excutorTools
+ .map((tool) => `${tool.name}: ${tool.description}`)
+ .join(', ')}`,
+ ),
+ Message.userMessage(environmentInfo),
+ Message.userMessage(
+ `Please call aware_analysis tool to give me next decision.`,
+ ),
+ ],
+ tools: [
+ {
+ type: 'function',
+ function: {
+ name: 'aware_analysis',
+ description:
+ 'Analyze the current environment with user input, and decide the next task status',
+ parameters: this.awareSchema,
+ },
+ },
+ ],
+ requestId: streamId,
+ });
+
+ if (!result.tool_calls?.length) {
+ console.warn('No tool calls returned');
+
+ // retry
+ try {
+ const res = JSON.parse(
+ jsonrepair(result.content || ''),
+ ) as AwareResult;
+ resolve(res);
+ } catch (e) {
+ throw new Error(`No tool calls returned ${result.content}`);
+ }
+ return;
+ }
+
+ const awareResult = JSON.parse(
+ result.tool_calls.filter(Boolean)[0].function.arguments,
+ ) as AwareResult;
+ resolve(awareResult);
+ } catch (error) {
+ reject(error);
+ } finally {
+ this.abortSignal.removeEventListener('abort', abortHandler);
+ }
+ });
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/agent/EventManager.ts b/apps/agent-tars/src/renderer/src/agent/EventManager.ts
new file mode 100644
index 000000000..91a01e21e
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/agent/EventManager.ts
@@ -0,0 +1,460 @@
+import { v4 as uuidv4 } from 'uuid';
+import {
+ EventItem,
+ EventType,
+ EventContentDescriptor,
+} from '@renderer/type/event';
+import { ActionStatus, PlanTask, ToolCallType } from '@renderer/type/agent';
+import { normalizeToolUsedInfo } from '@renderer/utils/normalizeToolUsedInfo';
+import { getLoadingTipFromToolCall } from '@renderer/utils/getLoadingTipForToolCall';
+import { ToolCall } from '@agent-infra/shared';
+import { SNAPSHOT_BROWSER_ACTIONS } from '@renderer/constants';
+
+export class EventManager {
+ private historyEvents: EventItem[] = [];
+ private events: EventItem[] = [];
+ private onEventsUpdate?: (events: EventItem[]) => void;
+
+ constructor(historyEvents: EventItem[] = []) {
+ this.historyEvents = historyEvents;
+ this.events = [];
+ }
+
+ /**
+ * Get history events
+ */
+ public getHistoryEvents(): EventItem[] {
+ return this.historyEvents;
+ }
+
+ /**
+ * Set callback function to be called whenever events are updated
+ */
+ public setUpdateCallback(callback: (events: EventItem[]) => void): void {
+ this.onEventsUpdate = callback;
+ }
+
+ /**
+ * Get all events
+ */
+ public getAllEvents(): EventItem[] {
+ return [...this.events];
+ }
+
+ /**
+ * Add a generic event
+ */
+ private async addEvent(
+ type: T,
+ content: EventContentDescriptor[T],
+ willNotifyUpdate = true,
+ ): Promise {
+ const event: EventItem = {
+ id: uuidv4(),
+ type,
+ content: content as EventContentDescriptor[keyof EventContentDescriptor],
+ timestamp: Date.now(),
+ };
+
+ this.events.push(event);
+ willNotifyUpdate && (await this.notifyUpdate());
+ return event;
+ }
+
+ /**
+ * Add a chat text event
+ */
+ public async addChatText(
+ content: string,
+ attachments: { path: string }[],
+ ): Promise {
+ return this.addEvent(EventType.ChatText, {
+ text: content,
+ attachments,
+ });
+ }
+
+ /**
+ * Add a loading status event
+ */
+ public async addLoadingStatus(
+ title: string,
+ willNotifyUpdate = true,
+ ): Promise {
+ return this.addEvent(EventType.LoadingStatus, { title }, willNotifyUpdate);
+ }
+
+ /**
+ * Add a plan update event
+ */
+ public async addPlanUpdate(
+ step: number,
+ plan: PlanTask[],
+ ): Promise {
+ return this.addEvent(EventType.PlanUpdate, {
+ plan,
+ step,
+ });
+ }
+
+ /**
+ * Add a new plan step event
+ */
+ public async addNewPlanStep(step: number): Promise {
+ return this.addEvent(EventType.NewPlanStep, { step });
+ }
+
+ /**
+ * Add an agent status event
+ */
+ public async addAgentStatus(status: string): Promise {
+ return this.addEvent(EventType.AgentStatus, status);
+ }
+
+ /**
+ * Add an observation event
+ */
+ public async addObservation(content: any): Promise {
+ return this.addEvent(EventType.Observation, content);
+ }
+
+ /**
+ * Update an existing event
+ */
+ public updateEvent(eventId: string, updates: Partial): boolean {
+ const eventIndex = this.events.findIndex((event) => event.id === eventId);
+ if (eventIndex === -1) return false;
+
+ this.events[eventIndex] = {
+ ...this.events[eventIndex],
+ ...updates,
+ // Don't allow overriding id
+ id: this.events[eventIndex].id,
+ };
+
+ this.notifyUpdate();
+ return true;
+ }
+
+ /**
+ * Update a tool used event status
+ */
+ public updateToolStatus(eventId: string, status: ActionStatus): boolean {
+ const event = this.events.find((e) => e.id === eventId);
+ if (!event || event.type !== EventType.ToolUsed) return false;
+
+ const content = event.content as EventContentDescriptor[EventType.ToolUsed];
+ return this.updateEvent(eventId, {
+ content: { ...content, status } as any,
+ });
+ }
+
+ /**
+ * Find events by type
+ */
+ public findEventsByType(type: T): EventItem[] {
+ return this.events.filter((event) => event.type === type);
+ }
+
+ /**
+ * Find the most recent event of a specific type
+ */
+ public findLatestEventByType(
+ type: T,
+ ): EventItem | undefined {
+ const events = this.findEventsByType(type);
+ return events.length > 0 ? events[events.length - 1] : undefined;
+ }
+
+ /**
+ * Clear all events
+ */
+ public clearEvents(): void {
+ this.events = [];
+ this.notifyUpdate();
+ }
+
+ /**
+ * Notify subscribers about event updates
+ */
+ private notifyUpdate(): void {
+ if (this.onEventsUpdate) {
+ this.onEventsUpdate(this.getAllEvents());
+ }
+ }
+
+ /**
+ * Add a user interrupt instruction event
+ */
+ public async addUserInteruptionInput(text: string): Promise {
+ return this.addEvent(EventType.UserInteruption, { text });
+ }
+
+ /**
+ * Add an end event
+ */
+ public async addEndEvent(message: string): Promise {
+ return this.addEvent(EventType.End, { message });
+ }
+
+ /**
+ * Add a tool call start event
+ */
+ public async addToolCallStart(
+ toolName: string,
+ params: string,
+ ): Promise {
+ const { value, description } = getLoadingTipFromToolCall(
+ toolName,
+ params,
+ ActionStatus.Running,
+ );
+ return this.addEvent(EventType.ToolCallStart, {
+ tool: toolName,
+ params,
+ description,
+ value,
+ });
+ }
+
+ /**
+ * Handle tool execution result and add related events
+ */
+ public async handleToolExecution({
+ toolName,
+ toolCallId,
+ params,
+ result,
+ isError,
+ }: {
+ toolName: string;
+ toolCallId: string;
+ params: string;
+ result: any;
+ isError: boolean;
+ }): Promise {
+ const normalizedInfo = normalizeToolUsedInfo(
+ toolName,
+ params,
+ isError ? ActionStatus.Failed : ActionStatus.Success,
+ result,
+ );
+
+ await this.addEvent(EventType.ToolUsed, {
+ actionId: toolCallId,
+ ...normalizedInfo,
+ });
+
+ await this.addObservation(JSON.stringify(result));
+ }
+
+ public async updateFileContentForEdit(originalContent: string) {
+ const latestEditEvent = [...this.events]
+ .reverse()
+ .find(
+ (event) =>
+ event.type === EventType.ToolUsed &&
+ (event.content.tool === ToolCallType.EditFile ||
+ event.content.tool === ToolCallType.WriteFile),
+ );
+
+ if (!latestEditEvent) {
+ return;
+ }
+
+ latestEditEvent.content = {
+ ...latestEditEvent.content,
+ original: originalContent,
+ };
+
+ await this.notifyUpdate();
+ }
+
+ public async updateScreenshot(screenshotFilePath: string) {
+ const latestBrowserNavigateEvent = [...this.events]
+ .reverse()
+ .find(
+ (event) =>
+ event.type === EventType.ToolUsed &&
+ SNAPSHOT_BROWSER_ACTIONS.includes(event.content.tool),
+ );
+
+ if (!latestBrowserNavigateEvent) {
+ return;
+ }
+
+ latestBrowserNavigateEvent.content = {
+ ...latestBrowserNavigateEvent.content,
+ result: [
+ ...latestBrowserNavigateEvent.content.result,
+ {
+ type: 'image',
+ path: screenshotFilePath,
+ },
+ ],
+ };
+
+ await this.notifyUpdate();
+ }
+
+ /**
+ * Add loading status for tool execution
+ */
+ public async addToolExecutionLoading(toolCall: ToolCall): Promise {
+ const { description } = getLoadingTipFromToolCall(
+ toolCall.function.name,
+ toolCall.function.arguments,
+ ActionStatus.Running,
+ );
+ return this.addLoadingStatus(description);
+ }
+
+ public async addUserMessageEvent(message: string): Promise {
+ return this.addEvent(EventType.UserMessage, message);
+ }
+
+ /**
+ * Normalize event stream for prompt context
+ * Limits both event count and context length
+ * Returns a string representation of relevant events
+ */
+ public normalizeEventsForPrompt(): string {
+ // Get last 1000 events
+ const recentEvents = [...this.historyEvents, ...this.events]
+ .filter((item) => item.type !== EventType.LoadingStatus)
+ .slice(-1000);
+ const MAX_CONTEXT_LENGTH = 50 * 1024 * 4; // 50KB * 4 (assumes 4 bytes per char)
+ let currentContextLength = 0;
+
+ // Process events from newest to oldest until context limit
+ const normalizedEvents: {
+ type: EventType;
+ content: Partial;
+ }[] = [];
+
+ for (let i = recentEvents.length - 1; i >= 0; i--) {
+ const event = recentEvents[i];
+ const normalizedEvent = this.normalizeEvent(event);
+
+ // Estimate content length
+ const eventContentLength = JSON.stringify(normalizedEvent).length * 4;
+
+ // Stop if adding this event would exceed context limit
+ if (currentContextLength + eventContentLength > MAX_CONTEXT_LENGTH) {
+ break;
+ }
+
+ normalizedEvents.unshift(normalizedEvent);
+ currentContextLength += eventContentLength;
+ }
+
+ // Convert normalized events to string format
+ return normalizedEvents
+ .map((event) => {
+ const { type, content } = event;
+ return `[${type}] ${JSON.stringify(content)}`;
+ })
+ .join('\n');
+ }
+
+ /**
+ * Helper method to normalize a single event
+ */
+ private normalizeEvent(event: EventItem): {
+ type: EventType;
+ content: Partial;
+ } {
+ const base = {
+ type: event.type,
+ content: {},
+ };
+
+ switch (event.type) {
+ case EventType.ToolUsed:
+ const content =
+ event.content as EventContentDescriptor[EventType.ToolUsed];
+ return {
+ ...base,
+ content: {
+ description: content.description,
+ status: content.status,
+ },
+ };
+
+ case EventType.ToolCallStart:
+ return {
+ ...base,
+ content: {
+ description: (
+ event.content as EventContentDescriptor[EventType.ToolCallStart]
+ ).description,
+ },
+ };
+
+ case EventType.ChatText:
+ case EventType.AgentStatus:
+ case EventType.Observation:
+ return {
+ ...base,
+ content: event.content,
+ };
+
+ case EventType.NewPlanStep:
+ return {
+ ...base,
+ content: {
+ step: (
+ event.content as EventContentDescriptor[EventType.NewPlanStep]
+ ).step,
+ },
+ };
+
+ case EventType.UserInteruption:
+ return {
+ ...base,
+ content: {
+ text: (
+ event.content as EventContentDescriptor[EventType.UserInteruption]
+ ).text,
+ },
+ };
+
+ case EventType.End:
+ return {
+ ...base,
+ content: {
+ message: (event.content as EventContentDescriptor[EventType.End])
+ .message,
+ },
+ };
+
+ default:
+ return base;
+ }
+ }
+
+ /**
+ * Add loading status for tool execution with custom loading message
+ */
+ public async updateToolExecutionLoadingMessage(
+ _toolCall: ToolCall,
+ message: string,
+ ): Promise {
+ // Find the latest loading status event for this tool
+ const loadingEvents = this.events
+ .filter((e) => e.type === EventType.LoadingStatus)
+ .reverse();
+
+ const latestLoadingEvent = loadingEvents[0];
+
+ if (latestLoadingEvent) {
+ // Update the loading message
+ this.updateEvent(latestLoadingEvent.id, {
+ content: { title: message } as any,
+ });
+ } else {
+ // Add a new loading status if none exists
+ await this.addLoadingStatus(message);
+ }
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/agent/Executor/index.ts b/apps/agent-tars/src/renderer/src/agent/Executor/index.ts
new file mode 100644
index 000000000..4172632b5
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/agent/Executor/index.ts
@@ -0,0 +1,155 @@
+import { AppContext } from '@renderer/hooks/useAgentFlow';
+import { AgentContext } from '../AgentFlow';
+import { ipcClient } from '@renderer/api';
+import { MCPServerName, Message, ToolCall } from '@agent-infra/shared';
+import { chatMessageTool, idleTool } from './tools';
+import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
+import { z } from 'zod';
+import { interceptToolCalls } from '@renderer/api/fileSystemInterceptor';
+
+export class Executor {
+ constructor(
+ private appContext: AppContext,
+ private agentContext: AgentContext,
+ private abortSignal: AbortSignal,
+ ) {}
+
+ private systemPrompt = `You are a tool use expert. You should call the appropriate tools according to the aware status and environment information.You should not output any response text and only return the JSON.
+
+
+- Must respond with a tool use (function calling); plain text responses are forbidden
+- Do not mention any specific tool names to users in messages
+- Carefully verify available tools; do not fabricate non-existent tools
+- Follow the instructions carefully in the aware status.
+- Don't repeat the same mean with aware status, you should select the appropriate tool.
+- Don't ask user anything, just tell user what you will do next.If some points is not very clear, you should tell user your solution.Don't ask user anything, remember, you are a agent for user.
+- You should only respond chat message after you have finished some tools and return the summary in chat message.
+- You should not output any response text and only return the tool call.
+- Don't output any file path in current machine and ensure the security in your message. Don't output any absolute path in your message.
+
+
+
+message as summary in current step.Don't return message first when the step just started.
+
+Notice, you should not output a lot of words in chat message, bacause the chat message is always summary words. If you want to write something in detail, please use \`write_file\` to write in by markdown file by default.
+
+In chat message tool, you should add the files that has been created in the past steps, and put the complete file path in the \`attachments\` param.
+
+
+
+If you meet the file system permission denied, you should check if the dir or file exists and create it if not.
+
+Before you interact with filesystem, you must list the allowed dirs and files and check if the dir or file exists and create it if not.Don't write file directly. You should write the file into a safe directory.
+
+
+
+
+After \`web_search\` called, then you must select web page from the search result, then you see the detail of the page, please call browser tool to do it.
+
+
+
+use \`browser_navigate\` to enter the page detail.
+use \`browser_scroll\` to scroll the page.When you use browser to enter the page detail, if the page content is partially visible, you should call browser tool to scroll to get more content, until the page content is fully visible.
+use \`browser_click\` to click the element.
+use \`browser_form_input_fill\` to fill the form.
+use \`browser_select\` to select the element.
+use \`browser_hover\` to hover the element.
+use \`browser_evaluate\` to evaluate the element.
+use \`browser_get_text\` to get the text of the element.
+
+
+
+When you use commands, you must cd the allowed dir instead of cwd.
+
+
+
+You should use the same language as the user input by default.
+ `;
+
+ public updateSignal(abortSignal: AbortSignal) {
+ this.abortSignal = abortSignal;
+ }
+
+ async run(status: string) {
+ const environmentInfo = await this.agentContext.getEnvironmentInfo(
+ this.appContext,
+ this.agentContext,
+ );
+
+ if (this.abortSignal.aborted) {
+ return [];
+ }
+
+ const streamId = Math.random().toString(36).substring(7);
+ return new Promise(async (resolve, reject) => {
+ const abortHandler = () => {
+ ipcClient.abortRequest({ requestId: streamId });
+ resolve([]);
+ };
+
+ try {
+ this.abortSignal.addEventListener('abort', abortHandler);
+ const result = await ipcClient.askLLMTool({
+ messages: [
+ Message.systemMessage(this.systemPrompt),
+ Message.userMessage(environmentInfo),
+ Message.userMessage(`Aware status: ${status}`),
+ ],
+ tools: [idleTool, chatMessageTool],
+ mcpServerKeys: [
+ MCPServerName.FileSystem,
+ MCPServerName.Commands,
+ MCPServerName.Browser,
+ ],
+ requestId: streamId,
+ });
+
+ const toolCalls = (result.tool_calls || []).filter(Boolean);
+
+ // Intercept tool calls to check file permissions - this will block if permission is needed
+ const interceptedToolCalls = await interceptToolCalls(toolCalls);
+
+ resolve(interceptedToolCalls);
+ } catch (error) {
+ reject(error);
+ } finally {
+ this.abortSignal.removeEventListener('abort', abortHandler);
+ }
+ });
+ }
+
+ async excuteTools(toolCalls: ToolCall[]) {
+ if (this.abortSignal.aborted) {
+ throw new DOMException('Aborted', 'AbortError');
+ }
+
+ return new Promise[]>(
+ async (resolve, reject) => {
+ const abortHandler = () => {
+ reject(new DOMException('Aborted', 'AbortError'));
+ };
+
+ try {
+ this.abortSignal.addEventListener('abort', abortHandler);
+
+ // Intercept tool calls to check file permissions - this will block if permission is needed
+ const interceptedToolCalls = await interceptToolCalls(toolCalls);
+
+ const result = await ipcClient.executeTool({
+ toolCalls: interceptedToolCalls,
+ });
+
+ console.log('Execute result', JSON.stringify(result));
+ if (this.abortSignal.aborted) {
+ throw new DOMException('Aborted', 'AbortError');
+ }
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ } finally {
+ this.abortSignal.removeEventListener('abort', abortHandler);
+ }
+ },
+ );
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/agent/Executor/tools.ts b/apps/agent-tars/src/renderer/src/agent/Executor/tools.ts
new file mode 100644
index 000000000..d4b628c4c
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/agent/Executor/tools.ts
@@ -0,0 +1,55 @@
+import { MCPServerName } from '@agent-infra/shared';
+import type { ChatCompletionTool } from 'openai/resources/chat';
+
+export enum ExecutorToolType {
+ FileSystem = MCPServerName.FileSystem,
+ Commands = MCPServerName.Commands,
+ Idle = 'idle',
+ ChatMessage = 'chat-message',
+}
+
+export const idleTool: ChatCompletionTool = {
+ type: 'function',
+ function: {
+ name: ExecutorToolType.Idle,
+ description:
+ 'If you find the current task is done, and current task is the last task, then you should call this tool to indicate that you are done.',
+ parameters: {
+ type: 'object',
+ properties: {},
+ },
+ },
+};
+
+export const chatMessageTool: ChatCompletionTool = {
+ type: 'function',
+ function: {
+ name: ExecutorToolType.ChatMessage,
+ description:
+ 'You can communicate with user by this tool. You should call this tool to output the response text to user.',
+ parameters: {
+ type: 'object',
+ properties: {
+ text: {
+ type: 'string',
+ description:
+ 'The response text to user, should be a summary of the current step, and should not be more than 150 words.',
+ },
+ attachments: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ path: {
+ type: 'string',
+ description:
+ 'The file path of the attachment that has been created in the past steps',
+ },
+ },
+ },
+ },
+ },
+ required: ['text'],
+ },
+ },
+};
diff --git a/apps/agent-tars/src/renderer/src/agent/Greeter.ts b/apps/agent-tars/src/renderer/src/agent/Greeter.ts
new file mode 100644
index 000000000..bfd873aa7
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/agent/Greeter.ts
@@ -0,0 +1,78 @@
+import { MessageType } from '@vendor/chat-ui';
+import { Message } from '@agent-infra/shared';
+import { ipcClient, onMainStreamEvent } from '@renderer/api';
+import { AppContext } from '@renderer/hooks/useAgentFlow';
+import { globalEventEmitter } from '@renderer/state/chat';
+
+export class Greeter {
+ constructor(
+ private appContext: AppContext,
+ private abortSignal: AbortSignal,
+ ) {}
+
+ async run() {
+ try {
+ let greetMessage = '';
+ const inputText = this.appContext.request.inputText;
+
+ const streamId = await ipcClient.askLLMTextStream({
+ messages: [
+ Message.systemMessage(
+ `You are a friendly greeter. Your role is to understand and empathize with users first. Listen carefully to their needs, acknowledge their concerns, and provide a warm, professional response. Before diving into the solution, express your understanding and confirm your commitment to help. Keep your initial response brief and encouraging, without detailing the specific steps you'll take.Don't ask user anything, just greet them.You should be very enthusiastic and positive.Give a warm and friendly greeting to the user.In the meantime, tell the user you will be ready to help them as soon as possible, let user know you are ready to help them.Don't ask user anything.`,
+ ),
+ Message.userMessage(inputText),
+ ],
+ requestId: Math.random().toString(36).substring(7),
+ });
+
+ await new Promise((resolve) => setTimeout(resolve, 200));
+
+ return new Promise((resolve, reject) => {
+ if (this.abortSignal.aborted) {
+ ipcClient.abortRequest({ requestId: streamId });
+ resolve('');
+ return;
+ }
+
+ let aborted = false;
+ globalEventEmitter.addListener(this.appContext.agentFlowId, (event) => {
+ if (event.type === 'terminate') {
+ ipcClient.abortRequest({ requestId: streamId });
+ resolve(greetMessage);
+ aborted = true;
+ cleanup();
+ }
+ });
+
+ const cleanup = onMainStreamEvent(streamId, {
+ onData: async (chunk: string) => {
+ if (aborted) {
+ return;
+ }
+ greetMessage += chunk;
+ await this.appContext.chatUtils.updateMessage(
+ {
+ type: MessageType.PlainText,
+ content: greetMessage,
+ },
+ {
+ shouldSyncStorage: true,
+ },
+ );
+ },
+ onError: (error: Error) => {
+ reject(error);
+ cleanup();
+ },
+ onEnd: async () => {
+ resolve(greetMessage);
+ cleanup();
+ },
+ });
+ });
+ } catch (error: any) {
+ console.log(error);
+ throw error;
+ }
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/agent/mockEvents.ts b/apps/agent-tars/src/renderer/src/agent/mockEvents.ts
new file mode 100644
index 000000000..227f1395d
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/agent/mockEvents.ts
@@ -0,0 +1,1958 @@
+export const events = [
+ {
+ id: '8b6b8b5d-3d5c-44b7-9abd-884b3481a618',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'doing',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'todo',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'todo',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 1,
+ },
+ timestamp: 1741905924301,
+ },
+ {
+ id: '594f2bfd-8d63-4fb4-a345-e792911c1e13',
+ type: 'agent-status',
+ content: '让我们从Python的基础环境搭建开始,首先需要下载并安装Python。',
+ timestamp: 1741905924301,
+ },
+ {
+ id: '37d8d56f-0643-4601-9a83-ea378cb8013b',
+ type: 'tool-call-start',
+ content: {
+ tool: 'run_command',
+ params: '{"command":"python3 --version"}\n',
+ description: 'Executing run command...',
+ value: 'python3 --version',
+ },
+ timestamp: 1741905927646,
+ },
+ {
+ id: '736314d6-dd68-4585-ae05-1f7eb43b713a',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_E6OsJZ0SQ3y5IXSc-qcObQ',
+ tool: 'run_command',
+ params: '{"command":"python3 --version"}\n',
+ description: 'Using ternimal to run command',
+ status: 'success',
+ value: 'python3 --version',
+ result: [
+ {
+ type: 'text',
+ text: 'Python 3.11.7\n',
+ name: 'STDOUT',
+ },
+ ],
+ },
+ timestamp: 1741905927895,
+ },
+ {
+ id: '6b19d10d-05a7-4c0b-bd3f-0ee89f821030',
+ type: 'observation',
+ content: '[{"type":"text","text":"Python 3.11.7\\n","name":"STDOUT"}]',
+ timestamp: 1741905927895,
+ },
+ {
+ id: '42decc81-9e5b-4ba9-bc86-c3df0f02597b',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'doing',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'todo',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'todo',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 1,
+ },
+ timestamp: 1741905934566,
+ },
+ {
+ id: 'e98481ba-9ef3-43c2-8f46-48f320d72972',
+ type: 'agent-status',
+ content:
+ '我们需要安装一个Python集成开发环境(IDE),推荐使用Visual Studio Code,这将帮助你更高效地编写Python代码。',
+ timestamp: 1741905934567,
+ },
+ {
+ id: '51a59bd5-f033-40fc-bc5a-afca2e153478',
+ type: 'tool-call-start',
+ content: {
+ tool: 'run_command',
+ params: '{"command":"which code"}\n',
+ description: 'Executing run command...',
+ value: 'which code',
+ },
+ timestamp: 1741905937667,
+ },
+ {
+ id: '436782d2-b35a-46d0-9856-1c520da772c9',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_aW5B2ch-Qfa0RQQ0rTrqVA',
+ tool: 'run_command',
+ params: '{"command":"which code"}\n',
+ description: 'Using ternimal to run command',
+ status: 'success',
+ value: 'which code',
+ result: [
+ {
+ type: 'text',
+ text: '/usr/local/bin/code\n',
+ name: 'STDOUT',
+ },
+ ],
+ },
+ timestamp: 1741905937687,
+ },
+ {
+ id: '199b62db-e9bb-48fb-bbd3-527cf31bf114',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"/usr/local/bin/code\\n","name":"STDOUT"}]',
+ timestamp: 1741905937687,
+ },
+ {
+ id: '9dfd8a8b-7057-4388-bdc6-3b0490a93ad1',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'doing',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'todo',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 2,
+ },
+ timestamp: 1741905947799,
+ },
+ {
+ id: '9a672ee7-5795-44da-b18d-9651373b2d27',
+ type: 'agent-status',
+ content:
+ '让我们开始学习Python的基础语法,请打开VSCode,创建一个新的Python文件,我们将从简单的print语句开始。',
+ timestamp: 1741905947799,
+ },
+ {
+ id: '4e509776-b477-4139-b633-a02bd1c9ead8',
+ type: 'tool-call-start',
+ content: {
+ tool: 'list_allowed_directories',
+ params: 'null\n',
+ description: 'Executing list allowed directories...',
+ value: 'executing...',
+ },
+ timestamp: 1741905950682,
+ },
+ {
+ id: '523d4fae-e5e3-4e0e-a313-8a2f74aa6fb9',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_98e2uPV6Tn-VO9u0srNzdA',
+ tool: 'list_allowed_directories',
+ params: 'null\n',
+ description: 'Using file system to list allowed directories',
+ status: 'success',
+ value: 'done',
+ result: [
+ {
+ type: 'text',
+ text: 'Allowed directories:\n/Users/bytedance/code/open-agent/app/temp',
+ },
+ ],
+ },
+ timestamp: 1741905950689,
+ },
+ {
+ id: 'be71298f-7ed7-4249-84fc-b247f4c9eab7',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"Allowed directories:\\n/Users/bytedance/code/open-agent/app/temp"}]',
+ timestamp: 1741905950689,
+ },
+ {
+ id: 'fe7fe97b-ae73-42d3-876b-bcd2314ab1cf',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'doing',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'todo',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 2,
+ },
+ timestamp: 1741905962397,
+ },
+ {
+ id: '3c4e861d-42ed-46aa-89e5-5b303336a859',
+ type: 'agent-status',
+ content:
+ '我将创建一个示例Python文件,向你展示Python的基本语法规则和注释的使用方法。',
+ timestamp: 1741905962397,
+ },
+ {
+ id: '5cb3bacf-20d1-4f57-901b-0cd63d05f2ab',
+ type: 'tool-call-start',
+ content: {
+ tool: 'create_directory',
+ params:
+ '{"path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial"}\n',
+ description: 'Executing create directory...',
+ value: '/Users/bytedance/code/open-agent/app/temp/python_tutorial',
+ },
+ timestamp: 1741905965678,
+ },
+ {
+ id: '3e2a5770-223d-46e8-8719-0e624b440b0f',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_eOHryav2RLeRikD4x0YGog',
+ tool: 'create_directory',
+ params:
+ '{"path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial"}\n',
+ description: 'Using file system to create directory',
+ status: 'success',
+ value: '/Users/bytedance/code/open-agent/app/temp/python_tutorial',
+ result: [
+ {
+ type: 'text',
+ text: 'Successfully created directory /Users/bytedance/code/open-agent/app/temp/python_tutorial',
+ },
+ ],
+ },
+ timestamp: 1741905965688,
+ },
+ {
+ id: '83f58768-da39-4e94-b935-8df5629a8043',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"Successfully created directory /Users/bytedance/code/open-agent/app/temp/python_tutorial"}]',
+ timestamp: 1741905965688,
+ },
+ {
+ id: 'da3cadda-58e7-48bf-ab5e-9f3aac08d954',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'doing',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'todo',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 2,
+ },
+ timestamp: 1741905979435,
+ },
+ {
+ id: '20d2e493-5c66-4b70-bd08-b1ee9910b600',
+ type: 'agent-status',
+ content: '我将创建一个基础Python示例文件来演示变量、数据类型和基本运算。',
+ timestamp: 1741905979435,
+ },
+ {
+ id: 'ef0132ee-d07a-4daf-8130-19a8bf6afb33',
+ type: 'tool-call-start',
+ content: {
+ tool: 'write_file',
+ params:
+ '{"content":"# 这是一个Python基础语法示例\\n\\n# 1. 打印输出\\nprint(\\"Hello, Python!\\") # 这是一个简单的打印语句\\n\\n# 2. 变量和基本数据类型\\nname = \\"小明\\" # 字符串类型\\nage = 18 # 整数类型\\nheight = 1.75 # 浮点数类型\\nis_student = True # 布尔类型\\n\\n# 3. 基本运算\\na = 10\\nb = 3\\nprint(\\"\\\\n基本运算示例:\\")\\nprint(\\"加法:\\", a + b) # 13\\nprint(\\"减法:\\", a - b) # 7\\nprint(\\"乘法:\\", a * b) # 30\\nprint(\\"除法:\\", a / b) # 3.3333...\\nprint(\\"整除:\\", a // b) # 3\\nprint(\\"取余:\\", a % b) # 1\\nprint(\\"幂运算:\\", a ** b) # 1000\\n\\n# 4. 字符串操作\\nprint(\\"\\\\n字符串操作示例:\\")\\nfirst_name = \\"Python\\"\\nlast_name = \\"编程\\"\\nfull_name = first_name + \\" \\" + last_name # 字符串拼接\\nprint(\\"完整名称:\\", full_name)\\nprint(\\"字符串重复:\\", \\"Python\\" * 3) # 重复字符串\\n\\n# 5. 类型转换\\nnum_str = \\"100\\"\\nnum_int = int(num_str) # 字符串转整数\\nprint(\\"\\\\n类型转换:\\", num_int + 50) # 可以进行数学运算","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/basic_syntax.py"}\n',
+ description: 'Executing write file...',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/basic_syntax.py',
+ },
+ timestamp: 1741905989879,
+ },
+ {
+ id: '1233d723-42bc-4df4-9ab2-27338eba4f62',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_dnvzYnS3Q2CX-UiyPzwTuQ',
+ tool: 'write_file',
+ params:
+ '{"content":"# 这是一个Python基础语法示例\\n\\n# 1. 打印输出\\nprint(\\"Hello, Python!\\") # 这是一个简单的打印语句\\n\\n# 2. 变量和基本数据类型\\nname = \\"小明\\" # 字符串类型\\nage = 18 # 整数类型\\nheight = 1.75 # 浮点数类型\\nis_student = True # 布尔类型\\n\\n# 3. 基本运算\\na = 10\\nb = 3\\nprint(\\"\\\\n基本运算示例:\\")\\nprint(\\"加法:\\", a + b) # 13\\nprint(\\"减法:\\", a - b) # 7\\nprint(\\"乘法:\\", a * b) # 30\\nprint(\\"除法:\\", a / b) # 3.3333...\\nprint(\\"整除:\\", a // b) # 3\\nprint(\\"取余:\\", a % b) # 1\\nprint(\\"幂运算:\\", a ** b) # 1000\\n\\n# 4. 字符串操作\\nprint(\\"\\\\n字符串操作示例:\\")\\nfirst_name = \\"Python\\"\\nlast_name = \\"编程\\"\\nfull_name = first_name + \\" \\" + last_name # 字符串拼接\\nprint(\\"完整名称:\\", full_name)\\nprint(\\"字符串重复:\\", \\"Python\\" * 3) # 重复字符串\\n\\n# 5. 类型转换\\nnum_str = \\"100\\"\\nnum_int = int(num_str) # 字符串转整数\\nprint(\\"\\\\n类型转换:\\", num_int + 50) # 可以进行数学运算","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/basic_syntax.py"}\n',
+ description: 'Using file system to write file',
+ status: 'success',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/basic_syntax.py',
+ result: [
+ {
+ type: 'text',
+ text: 'Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/basic_syntax.py',
+ },
+ ],
+ },
+ timestamp: 1741905989895,
+ },
+ {
+ id: '6a33a6b1-44c4-4a84-9595-f00061264a84',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/basic_syntax.py"}]',
+ timestamp: 1741905989895,
+ },
+ {
+ id: '1a4a8e12-8af3-4f88-96a2-d4bde3b52283',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'doing',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'todo',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 2,
+ },
+ timestamp: 1741905997226,
+ },
+ {
+ id: '546e9c1c-fef0-462c-9f58-0a81099094ab',
+ type: 'agent-status',
+ content:
+ '让我们运行basic_syntax.py文件,查看Python基础语法的运行结果和输出。',
+ timestamp: 1741905997226,
+ },
+ {
+ id: '49c4ef9d-d6a9-4e58-ba48-757c882c2f41',
+ type: 'tool-call-start',
+ content: {
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python basic_syntax.py"}\n',
+ description: 'Executing run command...',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python basic_syntax.py',
+ },
+ timestamp: 1741906000640,
+ },
+ {
+ id: '38d8b4a8-3eb8-4537-bc9f-bfcbb6dbb787',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_Xr2BvdeFSN6b37CRoP1uEQ',
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python basic_syntax.py"}\n',
+ description: 'Using ternimal to run command',
+ status: 'success',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python basic_syntax.py',
+ result: [
+ {
+ type: 'text',
+ text: 'Hello, Python!\n\n基本运算示例:\n加法: 13\n减法: 7\n乘法: 30\n除法: 3.3333333333333335\n整除: 3\n取余: 1\n幂运算: 1000\n\n字符串操作示例:\n完整名称: Python 编程\n字符串重复: PythonPythonPython\n\n类型转换: 150\n',
+ name: 'STDOUT',
+ },
+ ],
+ },
+ timestamp: 1741906000875,
+ },
+ {
+ id: 'e8d428bc-a2ab-498a-b0ff-3def11b20516',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"Hello, Python!\\n\\n基本运算示例:\\n加法: 13\\n减法: 7\\n乘法: 30\\n除法: 3.3333333333333335\\n整除: 3\\n取余: 1\\n幂运算: 1000\\n\\n字符串操作示例:\\n完整名称: Python 编程\\n字符串重复: PythonPythonPython\\n\\n类型转换: 150\\n","name":"STDOUT"}]',
+ timestamp: 1741906000875,
+ },
+ {
+ id: 'a21f81dc-a8eb-4b74-9d03-914a6c82c607',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'doing',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'todo',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 2,
+ },
+ timestamp: 1741906014694,
+ },
+ {
+ id: 'ac8b6273-8cbe-4f66-a858-7e53b3290ced',
+ type: 'agent-status',
+ content: '我将创建一个新的示例文件来演示Python的比较运算符和布尔运算。',
+ timestamp: 1741906014694,
+ },
+ {
+ id: '2bc09291-dac2-4efb-9cab-0ef0fe5d0242',
+ type: 'tool-call-start',
+ content: {
+ tool: 'write_file',
+ params:
+ '{"content":"# Python比较运算符和布尔运算示例\\n\\n# 比较运算符\\nx = 10\\ny = 5\\n\\nprint(\\"比较运算符示例:\\")\\nprint(f\\"{x} > {y}:\\", x > y) # 大于\\nprint(f\\"{x} < {y}:\\", x < y) # 小于\\nprint(f\\"{x} >= {y}:\\", x >= y) # 大于等于\\nprint(f\\"{x} <= {y}:\\", x <= y) # 小于等于\\nprint(f\\"{x} == {y}:\\", x == y) # 等于\\nprint(f\\"{x} != {y}:\\", x != y) # 不等于\\n\\nprint(\\"\\\\n布尔运算示例:\\")\\na = True\\nb = False\\n\\nprint(\\"a =\\", a)\\nprint(\\"b =\\", b)\\nprint(\\"a and b =\\", a and b) # 与运算\\nprint(\\"a or b =\\", a or b) # 或运算\\nprint(\\"not a =\\", not a) # 非运算\\n\\n# 复合条件示例\\nage = 25\\nhas_id = True\\n\\nprint(\\"\\\\n复合条件示例:\\")\\nprint(\\"年龄大于18且有身份证:\\", age >= 18 and has_id)\\nprint(\\"年龄小于18或没有身份证:\\", age < 18 or not has_id)","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/comparison_operators.py"}\n',
+ description: 'Executing write file...',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/comparison_operators.py',
+ },
+ timestamp: 1741906025586,
+ },
+ {
+ id: 'eb5e0d02-2856-4d70-859b-983eaa34ea5f',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_D3y7k_JsS7ChbrbemGTYBA',
+ tool: 'write_file',
+ params:
+ '{"content":"# Python比较运算符和布尔运算示例\\n\\n# 比较运算符\\nx = 10\\ny = 5\\n\\nprint(\\"比较运算符示例:\\")\\nprint(f\\"{x} > {y}:\\", x > y) # 大于\\nprint(f\\"{x} < {y}:\\", x < y) # 小于\\nprint(f\\"{x} >= {y}:\\", x >= y) # 大于等于\\nprint(f\\"{x} <= {y}:\\", x <= y) # 小于等于\\nprint(f\\"{x} == {y}:\\", x == y) # 等于\\nprint(f\\"{x} != {y}:\\", x != y) # 不等于\\n\\nprint(\\"\\\\n布尔运算示例:\\")\\na = True\\nb = False\\n\\nprint(\\"a =\\", a)\\nprint(\\"b =\\", b)\\nprint(\\"a and b =\\", a and b) # 与运算\\nprint(\\"a or b =\\", a or b) # 或运算\\nprint(\\"not a =\\", not a) # 非运算\\n\\n# 复合条件示例\\nage = 25\\nhas_id = True\\n\\nprint(\\"\\\\n复合条件示例:\\")\\nprint(\\"年龄大于18且有身份证:\\", age >= 18 and has_id)\\nprint(\\"年龄小于18或没有身份证:\\", age < 18 or not has_id)","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/comparison_operators.py"}\n',
+ description: 'Using file system to write file',
+ status: 'success',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/comparison_operators.py',
+ result: [
+ {
+ type: 'text',
+ text: 'Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/comparison_operators.py',
+ },
+ ],
+ },
+ timestamp: 1741906025595,
+ },
+ {
+ id: '66cba1fa-90fe-4a8b-b2fb-d89da0fbf4f9',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/comparison_operators.py"}]',
+ timestamp: 1741906025595,
+ },
+ {
+ id: '5563ded0-b831-442d-974e-6cefad864a6e',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'doing',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'todo',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 2,
+ },
+ timestamp: 1741906035063,
+ },
+ {
+ id: 'a30a482a-26ea-4aba-9da3-03c3f8c0aa32',
+ type: 'agent-status',
+ content:
+ '让我们运行comparison_operators.py文件,学习Python的比较运算符和布尔运算。',
+ timestamp: 1741906035063,
+ },
+ {
+ id: '123d0762-e9ad-493c-9ccd-0c22331f2688',
+ type: 'tool-call-start',
+ content: {
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python3 comparison_operators.py"}\n',
+ description: 'Executing run command...',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python3 comparison_operators.py',
+ },
+ timestamp: 1741906038278,
+ },
+ {
+ id: 'e3477776-11da-49e2-b57c-20fa466333ea',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_ZOG8qYpOTK-UqRs6C79YUA',
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python3 comparison_operators.py"}\n',
+ description: 'Using ternimal to run command',
+ status: 'success',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python3 comparison_operators.py',
+ result: [
+ {
+ type: 'text',
+ text: '比较运算符示例:\n10 > 5: True\n10 < 5: False\n10 >= 5: True\n10 <= 5: False\n10 == 5: False\n10 != 5: True\n\n布尔运算示例:\na = True\nb = False\na and b = False\na or b = True\nnot a = False\n\n复合条件示例:\n年龄大于18且有身份证: True\n年龄小于18或没有身份证: False\n',
+ name: 'STDOUT',
+ },
+ ],
+ },
+ timestamp: 1741906038347,
+ },
+ {
+ id: '0106f2b7-b5a2-46f0-b356-3751ea762710',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"比较运算符示例:\\n10 > 5: True\\n10 < 5: False\\n10 >= 5: True\\n10 <= 5: False\\n10 == 5: False\\n10 != 5: True\\n\\n布尔运算示例:\\na = True\\nb = False\\na and b = False\\na or b = True\\nnot a = False\\n\\n复合条件示例:\\n年龄大于18且有身份证: True\\n年龄小于18或没有身份证: False\\n","name":"STDOUT"}]',
+ timestamp: 1741906038347,
+ },
+ {
+ id: 'ffd37dff-8f49-430e-bd68-4fbcb34e1e9b',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'doing',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 3,
+ },
+ timestamp: 1741906046269,
+ },
+ {
+ id: 'f91a7684-f69f-4cd5-93d7-09bcda0e9f54',
+ type: 'agent-status',
+ content:
+ '让我们开始学习Python的数据类型和变量,我将创建一个新的示例文件来演示Python的各种数据类型。',
+ timestamp: 1741906046269,
+ },
+ {
+ id: 'b7809ee8-9d7b-4137-8f16-033adf04d4b3',
+ type: 'tool-call-start',
+ content: {
+ tool: 'write_file',
+ params:
+ '{"content":"# Python 数据类型示例\\n\\n# 1. 数值类型\\n# 整数 (int)\\nage = 25\\nprint(\\"整数示例:\\", age)\\n\\n# 浮点数 (float)\\nheight = 1.75\\nprint(\\"浮点数示例:\\", height)\\n\\n# 复数 (complex)\\ncomplex_num = 3 + 4j\\nprint(\\"复数示例:\\", complex_num)\\n\\n# 2. 字符串类型 (str)\\nname = \\"Python\\"\\ndescription = \'\'\'这是一个\\n多行字符串示例\'\'\'\\nprint(\\"\\\\n字符串示例:\\")\\nprint(\\"名称:\\", name)\\nprint(\\"描述:\\", description)\\n\\n# 3. 列表类型 (list) - 可变序列\\nfruits = [\\"苹果\\", \\"香蕉\\", \\"橙子\\"]\\nprint(\\"\\\\n列表示例:\\")\\nprint(\\"水果列表:\\", fruits)\\nprint(\\"第一个水果:\\", fruits[0])\\nfruits.append(\\"葡萄\\")\\nprint(\\"添加后的水果列表:\\", fruits)\\n\\n# 4. 元组类型 (tuple) - 不可变序列\\ncoordinates = (10, 20)\\nprint(\\"\\\\n元组示例:\\")\\nprint(\\"坐标:\\", coordinates)\\nprint(\\"x坐标:\\", coordinates[0])\\n\\n# 5. 字典类型 (dict) - 键值对\\nperson = {\\n \\"name\\": \\"张三\\",\\n \\"age\\": 30,\\n \\"city\\": \\"北京\\"\\n}\\nprint(\\"\\\\n字典示例:\\")\\nprint(\\"个人信息:\\", person)\\nprint(\\"姓名:\\", person[\\"name\\"])\\n\\n# 6. 集合类型 (set) - 无序不重复元素集合\\nnumbers = {1, 2, 3, 3, 4, 4, 5}\\nprint(\\"\\\\n集合示例:\\")\\nprint(\\"数字集合(重复元素会被自动去除):\\", numbers)\\n\\n# 7. 布尔类型 (bool)\\nis_student = True\\nis_working = False\\nprint(\\"\\\\n布尔类型示例:\\")\\nprint(\\"是学生吗?\\", is_student)\\nprint(\\"在工作吗?\\", is_working)\\n\\n# 8. 类型转换示例\\nprint(\\"\\\\n类型转换示例:\\")\\nnum_str = \\"100\\"\\nnum_int = int(num_str)\\nprint(\\"字符串转整数:\\", num_int)\\nprint(\\"整数转浮点数:\\", float(num_int))\\nprint(\\"数字转字符串:\\", str(num_int))\\n\\n# 9. 变量的特性\\nprint(\\"\\\\n变量的特性:\\")\\nx = 10\\ny = x # 变量赋值\\nprint(\\"x =\\", x)\\nprint(\\"y =\\", y)\\nx = 20 # 修改x的值\\nprint(\\"修改后:\\")\\nprint(\\"x =\\", x)\\nprint(\\"y =\\", y) # y的值不会改变\\n\\n# 10. None类型\\nempty_value = None\\nprint(\\"\\\\nNone类型示例:\\")\\nprint(\\"空值:\\", empty_value)","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/data_types.py"}\n',
+ description: 'Executing write file...',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/data_types.py',
+ },
+ timestamp: 1741906064958,
+ },
+ {
+ id: 'f9d64090-f094-40e7-ba78-46156319cc90',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_zhr8G6zZTjmmI8Iu2OdIiQ',
+ tool: 'write_file',
+ params:
+ '{"content":"# Python 数据类型示例\\n\\n# 1. 数值类型\\n# 整数 (int)\\nage = 25\\nprint(\\"整数示例:\\", age)\\n\\n# 浮点数 (float)\\nheight = 1.75\\nprint(\\"浮点数示例:\\", height)\\n\\n# 复数 (complex)\\ncomplex_num = 3 + 4j\\nprint(\\"复数示例:\\", complex_num)\\n\\n# 2. 字符串类型 (str)\\nname = \\"Python\\"\\ndescription = \'\'\'这是一个\\n多行字符串示例\'\'\'\\nprint(\\"\\\\n字符串示例:\\")\\nprint(\\"名称:\\", name)\\nprint(\\"描述:\\", description)\\n\\n# 3. 列表类型 (list) - 可变序列\\nfruits = [\\"苹果\\", \\"香蕉\\", \\"橙子\\"]\\nprint(\\"\\\\n列表示例:\\")\\nprint(\\"水果列表:\\", fruits)\\nprint(\\"第一个水果:\\", fruits[0])\\nfruits.append(\\"葡萄\\")\\nprint(\\"添加后的水果列表:\\", fruits)\\n\\n# 4. 元组类型 (tuple) - 不可变序列\\ncoordinates = (10, 20)\\nprint(\\"\\\\n元组示例:\\")\\nprint(\\"坐标:\\", coordinates)\\nprint(\\"x坐标:\\", coordinates[0])\\n\\n# 5. 字典类型 (dict) - 键值对\\nperson = {\\n \\"name\\": \\"张三\\",\\n \\"age\\": 30,\\n \\"city\\": \\"北京\\"\\n}\\nprint(\\"\\\\n字典示例:\\")\\nprint(\\"个人信息:\\", person)\\nprint(\\"姓名:\\", person[\\"name\\"])\\n\\n# 6. 集合类型 (set) - 无序不重复元素集合\\nnumbers = {1, 2, 3, 3, 4, 4, 5}\\nprint(\\"\\\\n集合示例:\\")\\nprint(\\"数字集合(重复元素会被自动去除):\\", numbers)\\n\\n# 7. 布尔类型 (bool)\\nis_student = True\\nis_working = False\\nprint(\\"\\\\n布尔类型示例:\\")\\nprint(\\"是学生吗?\\", is_student)\\nprint(\\"在工作吗?\\", is_working)\\n\\n# 8. 类型转换示例\\nprint(\\"\\\\n类型转换示例:\\")\\nnum_str = \\"100\\"\\nnum_int = int(num_str)\\nprint(\\"字符串转整数:\\", num_int)\\nprint(\\"整数转浮点数:\\", float(num_int))\\nprint(\\"数字转字符串:\\", str(num_int))\\n\\n# 9. 变量的特性\\nprint(\\"\\\\n变量的特性:\\")\\nx = 10\\ny = x # 变量赋值\\nprint(\\"x =\\", x)\\nprint(\\"y =\\", y)\\nx = 20 # 修改x的值\\nprint(\\"修改后:\\")\\nprint(\\"x =\\", x)\\nprint(\\"y =\\", y) # y的值不会改变\\n\\n# 10. None类型\\nempty_value = None\\nprint(\\"\\\\nNone类型示例:\\")\\nprint(\\"空值:\\", empty_value)","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/data_types.py"}\n',
+ description: 'Using file system to write file',
+ status: 'success',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/data_types.py',
+ result: [
+ {
+ type: 'text',
+ text: 'Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/data_types.py',
+ },
+ ],
+ },
+ timestamp: 1741906064975,
+ },
+ {
+ id: 'fdb6371a-2574-4846-8161-ed617af29ffe',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/data_types.py"}]',
+ timestamp: 1741906064976,
+ },
+ {
+ id: 'b5004433-372e-42e2-ae94-5495c35c057d',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'doing',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'todo',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 3,
+ },
+ timestamp: 1741906073615,
+ },
+ {
+ id: '411c3f77-eb48-445e-b353-095ca3dec436',
+ type: 'agent-status',
+ content:
+ '让我们运行data_types.py文件,学习Python的各种数据类型及其使用方法。',
+ timestamp: 1741906073615,
+ },
+ {
+ id: '5b072d7b-b013-4e21-84a5-0121a70bc145',
+ type: 'tool-call-start',
+ content: {
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python data_types.py"}\n',
+ description: 'Executing run command...',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python data_types.py',
+ },
+ timestamp: 1741906077354,
+ },
+ {
+ id: '012479e1-136c-40cd-8b43-b84569fee1e7',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_jyavuqOqQ725JIPqoBX03A',
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python data_types.py"}\n',
+ description: 'Using ternimal to run command',
+ status: 'success',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python data_types.py',
+ result: [
+ {
+ type: 'text',
+ text: "整数示例: 25\n浮点数示例: 1.75\n复数示例: (3+4j)\n\n字符串示例:\n名称: Python\n描述: 这是一个\n多行字符串示例\n\n列表示例:\n水果列表: ['苹果', '香蕉', '橙子']\n第一个水果: 苹果\n添加后的水果列表: ['苹果', '香蕉', '橙子', '葡萄']\n\n元组示例:\n坐标: (10, 20)\nx坐标: 10\n\n字典示例:\n个人信息: {'name': '张三', 'age': 30, 'city': '北京'}\n姓名: 张三\n\n集合示例:\n数字集合(重复元素会被自动去除): {1, 2, 3, 4, 5}\n\n布尔类型示例:\n是学生吗? True\n在工作吗? False\n\n类型转换示例:\n字符串转整数: 100\n整数转浮点数: 100.0\n数字转字符串: 100\n\n变量的特性:\nx = 10\ny = 10\n修改后:\nx = 20\ny = 10\n\nNone类型示例:\n空值: None\n",
+ name: 'STDOUT',
+ },
+ ],
+ },
+ timestamp: 1741906077442,
+ },
+ {
+ id: 'a59fbc1a-39d0-4aa7-a6dc-d1b1f631e17b',
+ type: 'observation',
+ content:
+ "[{\"type\":\"text\",\"text\":\"整数示例: 25\\n浮点数示例: 1.75\\n复数示例: (3+4j)\\n\\n字符串示例:\\n名称: Python\\n描述: 这是一个\\n多行字符串示例\\n\\n列表示例:\\n水果列表: ['苹果', '香蕉', '橙子']\\n第一个水果: 苹果\\n添加后的水果列表: ['苹果', '香蕉', '橙子', '葡萄']\\n\\n元组示例:\\n坐标: (10, 20)\\nx坐标: 10\\n\\n字典示例:\\n个人信息: {'name': '张三', 'age': 30, 'city': '北京'}\\n姓名: 张三\\n\\n集合示例:\\n数字集合(重复元素会被自动去除): {1, 2, 3, 4, 5}\\n\\n布尔类型示例:\\n是学生吗? True\\n在工作吗? False\\n\\n类型转换示例:\\n字符串转整数: 100\\n整数转浮点数: 100.0\\n数字转字符串: 100\\n\\n变量的特性:\\nx = 10\\ny = 10\\n修改后:\\nx = 20\\ny = 10\\n\\nNone类型示例:\\n空值: None\\n\",\"name\":\"STDOUT\"}]",
+ timestamp: 1741906077442,
+ },
+ {
+ id: '574273e0-2073-4c87-bc14-9fbbb4b82f0b',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'done',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'doing',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 4,
+ },
+ timestamp: 1741906086514,
+ },
+ {
+ id: 'c76e3d36-d571-4f3c-967e-6a32c0f8dea4',
+ type: 'agent-status',
+ content: '让我创建一个新的示例文件来演示Python的条件语句(if-elif-else)。',
+ timestamp: 1741906086515,
+ },
+ {
+ id: 'f7948054-4ea4-42f8-845b-f03c8ed53273',
+ type: 'tool-call-start',
+ content: {
+ tool: 'write_file',
+ params:
+ '{"content":"# Python条件语句示例\\n\\n# 简单的if语句\\nage = 18\\nif age >= 18:\\n print(\\"你已经成年了\\")\\n\\n# if-else语句\\nscore = 75\\nif score >= 60:\\n print(\\"考试及格\\")\\nelse:\\n print(\\"考试不及格\\")\\n\\n# if-elif-else语句\\ngrade = 85\\nprint(\\"\\\\n成绩等级判断:\\")\\nif grade >= 90:\\n print(\\"优秀\\")\\nelif grade >= 80:\\n print(\\"良好\\")\\nelif grade >= 60:\\n print(\\"及格\\")\\nelse:\\n print(\\"不及格\\")\\n\\n# 嵌套的if语句\\nprint(\\"\\\\n购票判断:\\")\\nage = 25\\nvip = True\\n\\nif age >= 18:\\n if vip:\\n print(\\"成人VIP票价:80元\\")\\n else:\\n print(\\"成人普通票价:100元\\")\\nelse:\\n if vip:\\n print(\\"儿童VIP票价:40元\\")\\n else:\\n print(\\"儿童普通票价:50元\\")\\n\\n# 使用and和or的复合条件\\ntemperature = 35\\nhumidity = 80\\n\\nprint(\\"\\\\n天气状况判断:\\")\\nif temperature > 30 and humidity > 60:\\n print(\\"天气闷热\\")\\nelif temperature > 30 or humidity > 60:\\n print(\\"天气不适宜\\")\\nelse:\\n print(\\"天气适宜\\")","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/conditional_statements.py"}\n',
+ description: 'Executing write file...',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/conditional_statements.py',
+ },
+ timestamp: 1741906097522,
+ },
+ {
+ id: 'b7bc89b8-e6ea-4e0c-a9ff-ae792ce34131',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_FSexS_elQDmkaVTswTriWA',
+ tool: 'write_file',
+ params:
+ '{"content":"# Python条件语句示例\\n\\n# 简单的if语句\\nage = 18\\nif age >= 18:\\n print(\\"你已经成年了\\")\\n\\n# if-else语句\\nscore = 75\\nif score >= 60:\\n print(\\"考试及格\\")\\nelse:\\n print(\\"考试不及格\\")\\n\\n# if-elif-else语句\\ngrade = 85\\nprint(\\"\\\\n成绩等级判断:\\")\\nif grade >= 90:\\n print(\\"优秀\\")\\nelif grade >= 80:\\n print(\\"良好\\")\\nelif grade >= 60:\\n print(\\"及格\\")\\nelse:\\n print(\\"不及格\\")\\n\\n# 嵌套的if语句\\nprint(\\"\\\\n购票判断:\\")\\nage = 25\\nvip = True\\n\\nif age >= 18:\\n if vip:\\n print(\\"成人VIP票价:80元\\")\\n else:\\n print(\\"成人普通票价:100元\\")\\nelse:\\n if vip:\\n print(\\"儿童VIP票价:40元\\")\\n else:\\n print(\\"儿童普通票价:50元\\")\\n\\n# 使用and和or的复合条件\\ntemperature = 35\\nhumidity = 80\\n\\nprint(\\"\\\\n天气状况判断:\\")\\nif temperature > 30 and humidity > 60:\\n print(\\"天气闷热\\")\\nelif temperature > 30 or humidity > 60:\\n print(\\"天气不适宜\\")\\nelse:\\n print(\\"天气适宜\\")","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/conditional_statements.py"}\n',
+ description: 'Using file system to write file',
+ status: 'success',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/conditional_statements.py',
+ result: [
+ {
+ type: 'text',
+ text: 'Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/conditional_statements.py',
+ },
+ ],
+ },
+ timestamp: 1741906097551,
+ },
+ {
+ id: 'cb814a10-e413-4f7a-a6fd-3f01ea674351',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/conditional_statements.py"}]',
+ timestamp: 1741906097552,
+ },
+ {
+ id: 'da519868-a90e-45b4-82a2-cfe4b47dc212',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'done',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'doing',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 4,
+ },
+ timestamp: 1741906104726,
+ },
+ {
+ id: 'df12aa35-c3b5-46ee-8b77-20a384ae03a0',
+ type: 'agent-status',
+ content:
+ '让我们运行conditional_statements.py文件,学习Python的条件语句(if-elif-else)的使用。',
+ timestamp: 1741906104726,
+ },
+ {
+ id: '7859decc-b4f9-4a61-894e-b2c97e6d2740',
+ type: 'tool-call-start',
+ content: {
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python conditional_statements.py"}\n',
+ description: 'Executing run command...',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python conditional_statements.py',
+ },
+ timestamp: 1741906108429,
+ },
+ {
+ id: '87cfeee5-0d73-4c74-b8f5-e581ac151455',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_jDuvA4MERGim0tud0LiKgQ',
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python conditional_statements.py"}\n',
+ description: 'Using ternimal to run command',
+ status: 'success',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python conditional_statements.py',
+ result: [
+ {
+ type: 'text',
+ text: '你已经成年了\n考试及格\n\n成绩等级判断:\n良好\n\n购票判断:\n成人VIP票价:80元\n\n天气状况判断:\n天气闷热\n',
+ name: 'STDOUT',
+ },
+ ],
+ },
+ timestamp: 1741906108511,
+ },
+ {
+ id: '0966d4c2-ddc8-4f1f-9780-c8d431258341',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"你已经成年了\\n考试及格\\n\\n成绩等级判断:\\n良好\\n\\n购票判断:\\n成人VIP票价:80元\\n\\n天气状况判断:\\n天气闷热\\n","name":"STDOUT"}]',
+ timestamp: 1741906108511,
+ },
+ {
+ id: 'c1bec99f-fca3-49c4-bf57-e1960aba1c42',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'done',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'doing',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 4,
+ },
+ timestamp: 1741906116602,
+ },
+ {
+ id: '1577a804-78e0-4e30-8f3a-66f15310abfe',
+ type: 'agent-status',
+ content: '我将创建一个新的示例文件来演示Python的循环语句(for和while)。',
+ timestamp: 1741906116602,
+ },
+ {
+ id: 'feef991a-c50b-4641-b681-6f2e3b7be90f',
+ type: 'tool-call-start',
+ content: {
+ tool: 'write_file',
+ params:
+ '{"content":"# for循环示例\\nprint(\\"for循环示例:\\")\\nprint(\\"\\\\n1. 遍历数字序列:\\")\\nfor i in range(5):\\n print(f\\"计数:{i}\\")\\n\\nprint(\\"\\\\n2. 遍历列表:\\")\\nfruits = [\\"苹果\\", \\"香蕉\\", \\"橙子\\"]\\nfor fruit in fruits:\\n print(f\\"水果:{fruit}\\")\\n\\nprint(\\"\\\\n3. 遍历字符串:\\")\\nname = \\"Python\\"\\nfor char in name:\\n print(f\\"字母:{char}\\")\\n\\nprint(\\"\\\\n4. 使用enumerate遍历带索引的列表:\\")\\nfor index, fruit in enumerate(fruits):\\n print(f\\"索引 {index}: {fruit}\\")\\n\\nprint(\\"\\\\n5. break语句示例:\\")\\nfor i in range(10):\\n if i == 5:\\n print(\\"遇到5,退出循环\\")\\n break\\n print(f\\"数字:{i}\\")\\n\\nprint(\\"\\\\n6. continue语句示例:\\")\\nfor i in range(5):\\n if i == 2:\\n print(\\"跳过2\\")\\n continue\\n print(f\\"数字:{i}\\")\\n\\nprint(\\"\\\\nwhile循环示例:\\")\\nprint(\\"\\\\n1. 基本while循环:\\")\\ncount = 0\\nwhile count < 5:\\n print(f\\"计数:{count}\\")\\n count += 1\\n\\nprint(\\"\\\\n2. while循环带break:\\")\\nnumber = 0\\nwhile True:\\n print(f\\"数字:{number}\\")\\n number += 1\\n if number >= 5:\\n print(\\"达到5,退出循环\\")\\n break\\n\\nprint(\\"\\\\n3. while循环带continue:\\")\\ni = 0\\nwhile i < 5:\\n i += 1\\n if i == 3:\\n print(\\"跳过3\\")\\n continue\\n print(f\\"数字:{i}\\")\\n\\n# 嵌套循环示例\\nprint(\\"\\\\n嵌套循环示例:\\")\\nfor i in range(3):\\n for j in range(3):\\n print(f\\"({i}, {j})\\", end=\\" \\")\\n print()","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/loops.py"}\n',
+ description: 'Executing write file...',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/loops.py',
+ },
+ timestamp: 1741906130155,
+ },
+ {
+ id: '7f351b06-b4f2-4158-9444-b45264383361',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_L6-kJc3RSjWAi4IN1RXMEw',
+ tool: 'write_file',
+ params:
+ '{"content":"# for循环示例\\nprint(\\"for循环示例:\\")\\nprint(\\"\\\\n1. 遍历数字序列:\\")\\nfor i in range(5):\\n print(f\\"计数:{i}\\")\\n\\nprint(\\"\\\\n2. 遍历列表:\\")\\nfruits = [\\"苹果\\", \\"香蕉\\", \\"橙子\\"]\\nfor fruit in fruits:\\n print(f\\"水果:{fruit}\\")\\n\\nprint(\\"\\\\n3. 遍历字符串:\\")\\nname = \\"Python\\"\\nfor char in name:\\n print(f\\"字母:{char}\\")\\n\\nprint(\\"\\\\n4. 使用enumerate遍历带索引的列表:\\")\\nfor index, fruit in enumerate(fruits):\\n print(f\\"索引 {index}: {fruit}\\")\\n\\nprint(\\"\\\\n5. break语句示例:\\")\\nfor i in range(10):\\n if i == 5:\\n print(\\"遇到5,退出循环\\")\\n break\\n print(f\\"数字:{i}\\")\\n\\nprint(\\"\\\\n6. continue语句示例:\\")\\nfor i in range(5):\\n if i == 2:\\n print(\\"跳过2\\")\\n continue\\n print(f\\"数字:{i}\\")\\n\\nprint(\\"\\\\nwhile循环示例:\\")\\nprint(\\"\\\\n1. 基本while循环:\\")\\ncount = 0\\nwhile count < 5:\\n print(f\\"计数:{count}\\")\\n count += 1\\n\\nprint(\\"\\\\n2. while循环带break:\\")\\nnumber = 0\\nwhile True:\\n print(f\\"数字:{number}\\")\\n number += 1\\n if number >= 5:\\n print(\\"达到5,退出循环\\")\\n break\\n\\nprint(\\"\\\\n3. while循环带continue:\\")\\ni = 0\\nwhile i < 5:\\n i += 1\\n if i == 3:\\n print(\\"跳过3\\")\\n continue\\n print(f\\"数字:{i}\\")\\n\\n# 嵌套循环示例\\nprint(\\"\\\\n嵌套循环示例:\\")\\nfor i in range(3):\\n for j in range(3):\\n print(f\\"({i}, {j})\\", end=\\" \\")\\n print()","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/loops.py"}\n',
+ description: 'Using file system to write file',
+ status: 'success',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/loops.py',
+ result: [
+ {
+ type: 'text',
+ text: 'Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/loops.py',
+ },
+ ],
+ },
+ timestamp: 1741906130182,
+ },
+ {
+ id: '14706e38-0e27-4513-8687-7bf9e9c91237',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/loops.py"}]',
+ timestamp: 1741906130182,
+ },
+ {
+ id: '15464214-8665-4446-b91f-ff82aa137f65',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'done',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'doing',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'todo',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 4,
+ },
+ timestamp: 1741906136968,
+ },
+ {
+ id: '8e05df64-7b7b-4981-b4c2-54e6dce66c36',
+ type: 'agent-status',
+ content:
+ '让我们运行loops.py文件,学习Python的循环语句(for和while)的使用方法。',
+ timestamp: 1741906136969,
+ },
+ {
+ id: '270f9869-1aa6-427f-a0b6-9a073ddf9897',
+ type: 'tool-call-start',
+ content: {
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python loops.py"}\n',
+ description: 'Executing run command...',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python loops.py',
+ },
+ timestamp: 1741906140203,
+ },
+ {
+ id: 'dfc13bbf-d08e-4989-8944-df56bf06a8e5',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_sugHWo-sQpW2GX67OR8aGw',
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python loops.py"}\n',
+ description: 'Using ternimal to run command',
+ status: 'success',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python loops.py',
+ result: [
+ {
+ type: 'text',
+ text: 'for循环示例:\n\n1. 遍历数字序列:\n计数:0\n计数:1\n计数:2\n计数:3\n计数:4\n\n2. 遍历列表:\n水果:苹果\n水果:香蕉\n水果:橙子\n\n3. 遍历字符串:\n字母:P\n字母:y\n字母:t\n字母:h\n字母:o\n字母:n\n\n4. 使用enumerate遍历带索引的列表:\n索引 0: 苹果\n索引 1: 香蕉\n索引 2: 橙子\n\n5. break语句示例:\n数字:0\n数字:1\n数字:2\n数字:3\n数字:4\n遇到5,退出循环\n\n6. continue语句示例:\n数字:0\n数字:1\n跳过2\n数字:3\n数字:4\n\nwhile循环示例:\n\n1. 基本while循环:\n计数:0\n计数:1\n计数:2\n计数:3\n计数:4\n\n2. while循环带break:\n数字:0\n数字:1\n数字:2\n数字:3\n数字:4\n达到5,退出循环\n\n3. while循环带continue:\n数字:1\n数字:2\n跳过3\n数字:4\n数字:5\n\n嵌套循环示例:\n(0, 0) (0, 1) (0, 2) \n(1, 0) (1, 1) (1, 2) \n(2, 0) (2, 1) (2, 2) \n',
+ name: 'STDOUT',
+ },
+ ],
+ },
+ timestamp: 1741906140275,
+ },
+ {
+ id: '3f5890bf-bd39-44f6-b77b-0bbbbb45ebe2',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"for循环示例:\\n\\n1. 遍历数字序列:\\n计数:0\\n计数:1\\n计数:2\\n计数:3\\n计数:4\\n\\n2. 遍历列表:\\n水果:苹果\\n水果:香蕉\\n水果:橙子\\n\\n3. 遍历字符串:\\n字母:P\\n字母:y\\n字母:t\\n字母:h\\n字母:o\\n字母:n\\n\\n4. 使用enumerate遍历带索引的列表:\\n索引 0: 苹果\\n索引 1: 香蕉\\n索引 2: 橙子\\n\\n5. break语句示例:\\n数字:0\\n数字:1\\n数字:2\\n数字:3\\n数字:4\\n遇到5,退出循环\\n\\n6. continue语句示例:\\n数字:0\\n数字:1\\n跳过2\\n数字:3\\n数字:4\\n\\nwhile循环示例:\\n\\n1. 基本while循环:\\n计数:0\\n计数:1\\n计数:2\\n计数:3\\n计数:4\\n\\n2. while循环带break:\\n数字:0\\n数字:1\\n数字:2\\n数字:3\\n数字:4\\n达到5,退出循环\\n\\n3. while循环带continue:\\n数字:1\\n数字:2\\n跳过3\\n数字:4\\n数字:5\\n\\n嵌套循环示例:\\n(0, 0) (0, 1) (0, 2) \\n(1, 0) (1, 1) (1, 2) \\n(2, 0) (2, 1) (2, 2) \\n","name":"STDOUT"}]',
+ timestamp: 1741906140275,
+ },
+ {
+ id: '65798fe9-2afa-489c-ba94-5fb5f8a8e4f7',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'done',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'done',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'doing',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'todo',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'todo',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'todo',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'todo',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'todo',
+ },
+ ],
+ step: 5,
+ },
+ timestamp: 1741906149363,
+ },
+ {
+ id: 'd8ad958c-ca07-468e-835d-ca77be4047a4',
+ type: 'agent-status',
+ content:
+ '让我们开始学习Python的函数和模块,我将创建示例来展示如何定义和使用函数。',
+ timestamp: 1741906149364,
+ },
+ {
+ id: 'ee4942bb-1a6e-4caf-86a7-d7b55c8f0134',
+ type: 'user-interuption',
+ content: {
+ text: '直接到最后一步',
+ },
+ timestamp: 1741906163649,
+ },
+ {
+ id: 'f2cca6a7-cc0c-474c-89dc-a65c40c65202',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'done',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'done',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'done',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'done',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'done',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'done',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'done',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'doing',
+ },
+ ],
+ step: 10,
+ },
+ timestamp: 1741906174863,
+ },
+ {
+ id: 'ed4c9281-c6a4-4809-839c-ef25819a61b1',
+ type: 'agent-status',
+ content:
+ '我将创建一个示例文件来展示Python的高级特性,包括装饰器、生成器、上下文管理器等内容。',
+ timestamp: 1741906174863,
+ },
+ {
+ id: 'a19ddd1e-d82e-42f9-acfa-4644a38535bb',
+ type: 'tool-call-start',
+ content: {
+ tool: 'write_file',
+ params:
+ '{"content":"#!/usr/bin/env python3\\n# -*- coding: utf-8 -*-\\n\\n\\"\\"\\"\\nPython高级特性示例\\n包含:装饰器、生成器、上下文管理器、属性装饰器等\\n\\"\\"\\"\\n\\nimport time\\nfrom contextlib import contextmanager\\n\\n# 1. 装饰器示例\\ndef timing_decorator(func):\\n def wrapper(*args, **kwargs):\\n start_time = time.time()\\n result = func(*args, **kwargs)\\n end_time = time.time()\\n print(f\\"函数 {func.__name__} 执行时间: {end_time - start_time:.4f} 秒\\")\\n return result\\n return wrapper\\n\\n@timing_decorator\\ndef slow_function():\\n \\"\\"\\"示例:被装饰的慢速函数\\"\\"\\"\\n time.sleep(1)\\n return \\"函数执行完成\\"\\n\\n# 2. 生成器示例\\ndef fibonacci_generator(n):\\n \\"\\"\\"生成斐波那契数列的生成器\\"\\"\\"\\n a, b = 0, 1\\n for _ in range(n):\\n yield a\\n a, b = b, a + b\\n\\n# 3. 上下文管理器示例\\n@contextmanager\\ndef timer_context():\\n \\"\\"\\"计时器上下文管理器\\"\\"\\"\\n start = time.time()\\n yield\\n end = time.time()\\n print(f\\"代码块执行时间: {end - start:.4f} 秒\\")\\n\\n# 4. 属性装饰器示例\\nclass Temperature:\\n def __init__(self, celsius=0):\\n self._celsius = celsius\\n\\n @property\\n def celsius(self):\\n return self._celsius\\n \\n @celsius.setter\\n def celsius(self, value):\\n if value < -273.15:\\n raise ValueError(\\"温度不能低于绝对零度\\")\\n self._celsius = value\\n \\n @property\\n def fahrenheit(self):\\n return self._celsius * 9/5 + 32\\n\\nprint(\\"1. 装饰器示例:\\")\\nresult = slow_function()\\nprint(result)\\nprint()\\n\\nprint(\\"2. 生成器示例:\\")\\nprint(\\"斐波那契数列前10个数:\\")\\nfor num in fibonacci_generator(10):\\n print(num, end=\\" \\")\\nprint(\\"\\\\n\\")\\n\\nprint(\\"3. 上下文管理器示例:\\")\\nwith timer_context():\\n # 模拟一些耗时操作\\n time.sleep(0.5)\\nprint()\\n\\nprint(\\"4. 属性装饰器示例:\\")\\ntemp = Temperature(25)\\nprint(f\\"摄氏度:{temp.celsius}°C\\")\\nprint(f\\"华氏度:{temp.fahrenheit}°F\\")\\ntemp.celsius = 30\\nprint(f\\"更新后的摄氏度:{temp.celsius}°C\\")\\nprint(f\\"更新后的华氏度:{temp.fahrenheit}°F\\")\\n\\ntry:\\n temp.celsius = -300 # 尝试设置一个无效温度\\nexcept ValueError as e:\\n print(f\\"错误:{e}\\")","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/advanced_features.py"}\n',
+ description: 'Executing write file...',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/advanced_features.py',
+ },
+ timestamp: 1741906192997,
+ },
+ {
+ id: 'ff175ce2-c89e-48d4-801e-fff76d8b2d32',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_Bn98YAYBS0OcPOz7A5AD8Q',
+ tool: 'write_file',
+ params:
+ '{"content":"#!/usr/bin/env python3\\n# -*- coding: utf-8 -*-\\n\\n\\"\\"\\"\\nPython高级特性示例\\n包含:装饰器、生成器、上下文管理器、属性装饰器等\\n\\"\\"\\"\\n\\nimport time\\nfrom contextlib import contextmanager\\n\\n# 1. 装饰器示例\\ndef timing_decorator(func):\\n def wrapper(*args, **kwargs):\\n start_time = time.time()\\n result = func(*args, **kwargs)\\n end_time = time.time()\\n print(f\\"函数 {func.__name__} 执行时间: {end_time - start_time:.4f} 秒\\")\\n return result\\n return wrapper\\n\\n@timing_decorator\\ndef slow_function():\\n \\"\\"\\"示例:被装饰的慢速函数\\"\\"\\"\\n time.sleep(1)\\n return \\"函数执行完成\\"\\n\\n# 2. 生成器示例\\ndef fibonacci_generator(n):\\n \\"\\"\\"生成斐波那契数列的生成器\\"\\"\\"\\n a, b = 0, 1\\n for _ in range(n):\\n yield a\\n a, b = b, a + b\\n\\n# 3. 上下文管理器示例\\n@contextmanager\\ndef timer_context():\\n \\"\\"\\"计时器上下文管理器\\"\\"\\"\\n start = time.time()\\n yield\\n end = time.time()\\n print(f\\"代码块执行时间: {end - start:.4f} 秒\\")\\n\\n# 4. 属性装饰器示例\\nclass Temperature:\\n def __init__(self, celsius=0):\\n self._celsius = celsius\\n\\n @property\\n def celsius(self):\\n return self._celsius\\n \\n @celsius.setter\\n def celsius(self, value):\\n if value < -273.15:\\n raise ValueError(\\"温度不能低于绝对零度\\")\\n self._celsius = value\\n \\n @property\\n def fahrenheit(self):\\n return self._celsius * 9/5 + 32\\n\\nprint(\\"1. 装饰器示例:\\")\\nresult = slow_function()\\nprint(result)\\nprint()\\n\\nprint(\\"2. 生成器示例:\\")\\nprint(\\"斐波那契数列前10个数:\\")\\nfor num in fibonacci_generator(10):\\n print(num, end=\\" \\")\\nprint(\\"\\\\n\\")\\n\\nprint(\\"3. 上下文管理器示例:\\")\\nwith timer_context():\\n # 模拟一些耗时操作\\n time.sleep(0.5)\\nprint()\\n\\nprint(\\"4. 属性装饰器示例:\\")\\ntemp = Temperature(25)\\nprint(f\\"摄氏度:{temp.celsius}°C\\")\\nprint(f\\"华氏度:{temp.fahrenheit}°F\\")\\ntemp.celsius = 30\\nprint(f\\"更新后的摄氏度:{temp.celsius}°C\\")\\nprint(f\\"更新后的华氏度:{temp.fahrenheit}°F\\")\\n\\ntry:\\n temp.celsius = -300 # 尝试设置一个无效温度\\nexcept ValueError as e:\\n print(f\\"错误:{e}\\")","path":"/Users/bytedance/code/open-agent/app/temp/python_tutorial/advanced_features.py"}\n',
+ description: 'Using file system to write file',
+ status: 'success',
+ value:
+ '/Users/bytedance/code/open-agent/app/temp/python_tutorial/advanced_features.py',
+ result: [
+ {
+ type: 'text',
+ text: 'Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/advanced_features.py',
+ },
+ ],
+ },
+ timestamp: 1741906193047,
+ },
+ {
+ id: 'd7648063-736e-4a07-bbad-47b2f2de17fa',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"Successfully wrote to /Users/bytedance/code/open-agent/app/temp/python_tutorial/advanced_features.py"}]',
+ timestamp: 1741906193048,
+ },
+ {
+ id: '30e4b039-6c28-4b98-a2ee-3ef772ee8b9c',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'done',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'done',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'done',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'done',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'done',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'done',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'done',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'doing',
+ },
+ ],
+ step: 10,
+ },
+ timestamp: 1741906199428,
+ },
+ {
+ id: 'f179f65b-6a28-4713-8ea9-73968f16759c',
+ type: 'agent-status',
+ content:
+ '让我们运行advanced_features.py文件,学习Python的高级特性和最佳实践。',
+ timestamp: 1741906199428,
+ },
+ {
+ id: '8dac5c5f-4404-4c89-b01a-7b5c1574b02d',
+ type: 'tool-call-start',
+ content: {
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python advanced_features.py"}\n',
+ description: 'Executing run command...',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python advanced_features.py',
+ },
+ timestamp: 1741906203559,
+ },
+ {
+ id: 'dd496c34-93e0-40b7-a4a8-bff290920d5a',
+ type: 'tool-used',
+ content: {
+ actionId: 'tooluse_b_9v8no7QWueMlP6YNqv-w',
+ tool: 'run_command',
+ params:
+ '{"command":"cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python advanced_features.py"}\n',
+ description: 'Using ternimal to run command',
+ status: 'success',
+ value:
+ 'cd /Users/bytedance/code/open-agent/app/temp/python_tutorial && python advanced_features.py',
+ result: [
+ {
+ type: 'text',
+ text: '1. 装饰器示例:\n函数 slow_function 执行时间: 1.0017 秒\n函数执行完成\n\n2. 生成器示例:\n斐波那契数列前10个数:\n0 1 1 2 3 5 8 13 21 34 \n\n3. 上下文管理器示例:\n代码块执行时间: 0.5051 秒\n\n4. 属性装饰器示例:\n摄氏度:25°C\n华氏度:77.0°F\n更新后的摄氏度:30°C\n更新后的华氏度:86.0°F\n错误:温度不能低于绝对零度\n',
+ name: 'STDOUT',
+ },
+ ],
+ },
+ timestamp: 1741906205169,
+ },
+ {
+ id: 'f5cc4833-4201-40a7-aaa9-8cf4020a0673',
+ type: 'observation',
+ content:
+ '[{"type":"text","text":"1. 装饰器示例:\\n函数 slow_function 执行时间: 1.0017 秒\\n函数执行完成\\n\\n2. 生成器示例:\\n斐波那契数列前10个数:\\n0 1 1 2 3 5 8 13 21 34 \\n\\n3. 上下文管理器示例:\\n代码块执行时间: 0.5051 秒\\n\\n4. 属性装饰器示例:\\n摄氏度:25°C\\n华氏度:77.0°F\\n更新后的摄氏度:30°C\\n更新后的华氏度:86.0°F\\n错误:温度不能低于绝对零度\\n","name":"STDOUT"}]',
+ timestamp: 1741906205169,
+ },
+ {
+ id: '31a4d9fa-d9c9-4784-a9b1-e4d8407c764e',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'done',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'done',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'done',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'done',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'done',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'done',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'done',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'doing',
+ },
+ ],
+ step: 10,
+ },
+ timestamp: 1741906213931,
+ },
+ {
+ id: 'ce3642cb-63ac-4287-af18-146804c4ad6d',
+ type: 'agent-status',
+ content:
+ '恭喜你已经完成了Python的基础学习!建议你现在可以开始一个实际的项目来运用所学知识。',
+ timestamp: 1741906213931,
+ },
+ {
+ id: '69c990e8-2553-4723-bcfb-ba2a265508f4',
+ type: 'tool-call-start',
+ content: {
+ tool: 'chat-message',
+ params:
+ '{"text":"恭喜你已经完成了Python的全部基础学习!你现在已经掌握了包括基础语法、数据类型、控制流程、函数、面向对象编程,以及高级特性(如装饰器、生成器和上下文管理器)等知识。建议你接下来可以:\\n\\n1. 选择一个实际项目来练习,比如开发一个简单的网站、爬虫或数据分析应用\\n2. 深入学习Python的一些专业领域,如Web开发、数据科学或人工智能\\n3. 参与开源项目来提升实战经验\\n\\n记住,编程最重要的是实践,多写代码,多解决实际问题!"}\n',
+ description: 'Executing chat-message...',
+ value: 'executing...',
+ },
+ timestamp: 1741906222165,
+ },
+ {
+ id: 'ac49a988-e803-4ad6-8cfa-a93d4160af6c',
+ type: 'chat-text',
+ content:
+ '恭喜你已经完成了Python的全部基础学习!你现在已经掌握了包括基础语法、数据类型、控制流程、函数、面向对象编程,以及高级特性(如装饰器、生成器和上下文管理器)等知识。建议你接下来可以:\n\n1. 选择一个实际项目来练习,比如开发一个简单的网站、爬虫或数据分析应用\n2. 深入学习Python的一些专业领域,如Web开发、数据科学或人工智能\n3. 参与开源项目来提升实战经验\n\n记住,编程最重要的是实践,多写代码,多解决实际问题!',
+ timestamp: 1741906222165,
+ },
+ {
+ id: '48d8c516-a73f-4ce5-ae43-544a52e3db49',
+ type: 'tool-call-start',
+ content: {
+ tool: 'idle',
+ params: 'null\n',
+ description: 'Executing idle...',
+ value: 'executing...',
+ },
+ timestamp: 1741906222165,
+ },
+ {
+ id: 'e9df1e70-5de3-4180-b77e-85ab87362900',
+ type: 'plan-update',
+ content: {
+ plan: [
+ {
+ id: 'step_001',
+ title: '搭建Python开发环境',
+ status: 'done',
+ },
+ {
+ id: 'step_002',
+ title: '学习Python基础语法',
+ status: 'done',
+ },
+ {
+ id: 'step_003',
+ title: '掌握数据类型和变量',
+ status: 'done',
+ },
+ {
+ id: 'step_004',
+ title: '理解控制流程',
+ status: 'done',
+ },
+ {
+ id: 'step_005',
+ title: '学习函数和模块',
+ status: 'done',
+ },
+ {
+ id: 'step_006',
+ title: '了解面向对象编程',
+ status: 'done',
+ },
+ {
+ id: 'step_007',
+ title: '实践文件操作和异常处理',
+ status: 'done',
+ },
+ {
+ id: 'step_008',
+ title: '学习常用标准库',
+ status: 'done',
+ },
+ {
+ id: 'step_009',
+ title: '完成基础项目实战',
+ status: 'done',
+ },
+ {
+ id: 'step_010',
+ title: '学习高级特性和最佳实践',
+ status: 'done',
+ },
+ ],
+ step: 10,
+ },
+ timestamp: 1741906222166,
+ },
+ {
+ id: 'f65c6a4d-3bab-4246-a4b1-f45344133518',
+ type: 'end',
+ content: {
+ message: '> Agent TARS Agent has finished.',
+ },
+ timestamp: 1741906222166,
+ },
+];
diff --git a/apps/agent-tars/src/renderer/src/api/fileSystemInterceptor.ts b/apps/agent-tars/src/renderer/src/api/fileSystemInterceptor.ts
new file mode 100644
index 000000000..be4cc1d58
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/api/fileSystemInterceptor.ts
@@ -0,0 +1,116 @@
+import { ToolCallType } from '@renderer/type/agent';
+import {
+ checkPathPermission,
+ normalizePath,
+} from '@renderer/services/filePermissionService';
+import toast from 'react-hot-toast';
+import { ToolCall } from '@agent-infra/shared';
+
+// File operation tools that require path permission checks
+const FILE_OPERATION_TOOLS = [
+ ToolCallType.ReadFile,
+ ToolCallType.WriteFile,
+ ToolCallType.ReadMultipleFiles,
+ ToolCallType.EditFile,
+ ToolCallType.CreateDirectory,
+ ToolCallType.ListDirectory,
+ ToolCallType.DirectoryTree,
+ ToolCallType.MoveFile,
+ ToolCallType.SearchFiles,
+ ToolCallType.GetFileInfo,
+];
+
+/**
+ * Intercepts tool calls to check file path permissions
+ * @param toolCalls The tool calls to intercept
+ * @returns Promise resolving to the intercepted tool calls
+ */
+export async function interceptToolCalls(
+ toolCalls: ToolCall[],
+): Promise {
+ const interceptedCalls: ToolCall[] = [];
+
+ for (const toolCall of toolCalls) {
+ const toolName = toolCall.function.name as ToolCallType;
+
+ // Skip tools that don't need path permission checks
+ if (!FILE_OPERATION_TOOLS.includes(toolName)) {
+ interceptedCalls.push(toolCall);
+ continue;
+ }
+
+ try {
+ const params = JSON.parse(toolCall.function.arguments);
+
+ // Check which paths need permission
+ let pathsToCheck: string[] = [];
+
+ if (
+ toolName === ToolCallType.ReadFile ||
+ toolName === ToolCallType.WriteFile ||
+ toolName === ToolCallType.EditFile ||
+ toolName === ToolCallType.CreateDirectory ||
+ toolName === ToolCallType.ListDirectory ||
+ toolName === ToolCallType.DirectoryTree ||
+ toolName === ToolCallType.GetFileInfo
+ ) {
+ pathsToCheck = [params.path];
+ } else if (toolName === ToolCallType.ReadMultipleFiles) {
+ pathsToCheck = params.paths || [];
+ } else if (toolName === ToolCallType.MoveFile) {
+ pathsToCheck = [params.source, params.destination];
+ } else if (toolName === ToolCallType.SearchFiles) {
+ pathsToCheck = [params.path];
+ }
+
+ // Check permissions for all paths - this will now block until user decides
+ let allPathsAllowed = true;
+ for (const pathToCheck of pathsToCheck) {
+ const allowed = await checkPathPermission(pathToCheck);
+ if (!allowed) {
+ allPathsAllowed = false;
+ toast.error(`Access denied to path: ${pathToCheck}`);
+ break;
+ }
+ }
+
+ if (allPathsAllowed) {
+ // If all paths are allowed, normalize them in the tool call
+ const updatedParams = { ...params };
+
+ if (
+ toolName === ToolCallType.ReadFile ||
+ toolName === ToolCallType.WriteFile ||
+ toolName === ToolCallType.EditFile ||
+ toolName === ToolCallType.CreateDirectory ||
+ toolName === ToolCallType.ListDirectory ||
+ toolName === ToolCallType.DirectoryTree ||
+ toolName === ToolCallType.GetFileInfo
+ ) {
+ updatedParams.path = normalizePath(params.path);
+ } else if (toolName === ToolCallType.ReadMultipleFiles) {
+ updatedParams.paths = (params.paths || []).map(normalizePath);
+ } else if (toolName === ToolCallType.MoveFile) {
+ updatedParams.source = normalizePath(params.source);
+ updatedParams.destination = normalizePath(params.destination);
+ } else if (toolName === ToolCallType.SearchFiles) {
+ updatedParams.path = normalizePath(params.path);
+ }
+
+ // Update the tool call with normalized paths
+ interceptedCalls.push({
+ ...toolCall,
+ function: {
+ ...toolCall.function,
+ arguments: JSON.stringify(updatedParams),
+ },
+ });
+ }
+ } catch (error) {
+ console.error(`Error intercepting tool call ${toolName}:`, error);
+ interceptedCalls.push(toolCall);
+ }
+ }
+
+ return interceptedCalls;
+}
diff --git a/apps/agent-tars/src/renderer/src/api/index.ts b/apps/agent-tars/src/renderer/src/api/index.ts
new file mode 100644
index 000000000..c2b531625
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/api/index.ts
@@ -0,0 +1,29 @@
+import { createClient } from '@ui-tars/electron-ipc/renderer';
+import type { Router } from '../../../main/ipcRoutes';
+
+export const ipcClient = createClient({
+ ipcInvoke: window.electron.ipcRenderer.invoke,
+});
+
+export const onMainStreamEvent = (
+ streamId: string,
+ handlers: {
+ onData: (chunk: string) => void;
+ onError: (error: Error) => void;
+ onEnd: () => void;
+ },
+) => {
+ const dataListener = (data: string) => handlers.onData(data);
+ const errorListener = (error: Error) => handlers.onError(error);
+ const endListener = () => handlers.onEnd();
+
+ window.api.on(`llm:stream:${streamId}:data`, dataListener);
+ window.api.on(`llm:stream:${streamId}:error`, errorListener);
+ window.api.on(`llm:stream:${streamId}:end`, endListener);
+
+ return () => {
+ window.api.off(`llm:stream:${streamId}:data`, dataListener);
+ window.api.off(`llm:stream:${streamId}:error`, errorListener);
+ window.api.off(`llm:stream:${streamId}:end`, endListener);
+ };
+};
diff --git a/apps/agent-tars/src/renderer/src/api/llmConfig.ts b/apps/agent-tars/src/renderer/src/api/llmConfig.ts
new file mode 100644
index 000000000..343c228c7
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/api/llmConfig.ts
@@ -0,0 +1,30 @@
+import { ipcClient } from './index';
+import { ModelSettings } from '@agent-infra/shared';
+import { getLLMProviderConfig } from '../services/llmSettings';
+
+/**
+ * Update the LLM configuration in the main process
+ */
+export async function updateLLMConfig(
+ settings: ModelSettings,
+): Promise {
+ try {
+ const config = getLLMProviderConfig(settings);
+ return await ipcClient.updateLLMConfig(config);
+ } catch (error) {
+ console.error('Failed to update LLM configuration:', error);
+ return false;
+ }
+}
+
+/**
+ * Get available LLM providers from the main process
+ */
+export async function getAvailableProviders(): Promise {
+ try {
+ return await ipcClient.getAvailableProviders();
+ } catch (error) {
+ console.error('Failed to get available providers:', error);
+ return [];
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/assets/Mistral.tsx b/apps/agent-tars/src/renderer/src/assets/Mistral.tsx
new file mode 100644
index 000000000..84f9f3e2b
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/assets/Mistral.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+
+interface MistralIconProps {
+ className?: string;
+ width?: string | number;
+ height?: string | number;
+}
+
+const MistralIcon: React.FC = ({
+ className,
+ width = '1em',
+ height = '1em',
+}) => {
+ return (
+
+ Mistral
+
+
+
+
+
+
+ );
+};
+
+export default MistralIcon;
diff --git a/apps/agent-tars/src/renderer/src/assets/base.css b/apps/agent-tars/src/renderer/src/assets/base.css
new file mode 100644
index 000000000..5ed6406a3
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/assets/base.css
@@ -0,0 +1,67 @@
+:root {
+ --ev-c-white: #ffffff;
+ --ev-c-white-soft: #f8f8f8;
+ --ev-c-white-mute: #f2f2f2;
+
+ --ev-c-black: #1b1b1f;
+ --ev-c-black-soft: #222222;
+ --ev-c-black-mute: #282828;
+
+ --ev-c-gray-1: #515c67;
+ --ev-c-gray-2: #414853;
+ --ev-c-gray-3: #32363f;
+
+ --ev-c-text-1: rgba(255, 255, 245, 0.86);
+ --ev-c-text-2: rgba(235, 235, 245, 0.6);
+ --ev-c-text-3: rgba(235, 235, 245, 0.38);
+
+ --ev-button-alt-border: transparent;
+ --ev-button-alt-text: var(--ev-c-text-1);
+ --ev-button-alt-bg: var(--ev-c-gray-3);
+ --ev-button-alt-hover-border: transparent;
+ --ev-button-alt-hover-text: var(--ev-c-text-1);
+ --ev-button-alt-hover-bg: var(--ev-c-gray-2);
+}
+
+:root {
+ --color-background: var(--ev-c-black);
+ --color-background-soft: var(--ev-c-black-soft);
+ --color-background-mute: var(--ev-c-black-mute);
+
+ --color-text: var(--ev-c-text-1);
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ font-weight: normal;
+}
+
+ul {
+ list-style: none;
+}
+
+body {
+ min-height: 100vh;
+ color: var(--color-text);
+ background: var(--color-background);
+ line-height: 1.6;
+ font-family:
+ Inter,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ Roboto,
+ Oxygen,
+ Ubuntu,
+ Cantarell,
+ 'Fira Sans',
+ 'Droid Sans',
+ 'Helvetica Neue',
+ sans-serif;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
diff --git a/apps/agent-tars/src/renderer/src/assets/electron.svg b/apps/agent-tars/src/renderer/src/assets/electron.svg
new file mode 100644
index 000000000..45ef09cf4
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/assets/electron.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/agent-tars/src/renderer/src/assets/logo.png b/apps/agent-tars/src/renderer/src/assets/logo.png
new file mode 100644
index 000000000..6e9a3a185
Binary files /dev/null and b/apps/agent-tars/src/renderer/src/assets/logo.png differ
diff --git a/apps/agent-tars/src/renderer/src/assets/main.css b/apps/agent-tars/src/renderer/src/assets/main.css
new file mode 100644
index 000000000..0179fc4c2
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/assets/main.css
@@ -0,0 +1,171 @@
+@import './base.css';
+
+body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ background-image: url('./wavy-lines.svg');
+ background-size: cover;
+ user-select: none;
+}
+
+code {
+ font-weight: 600;
+ padding: 3px 5px;
+ border-radius: 2px;
+ background-color: var(--color-background-mute);
+ font-family:
+ ui-monospace,
+ SFMono-Regular,
+ SF Mono,
+ Menlo,
+ Consolas,
+ Liberation Mono,
+ monospace;
+ font-size: 85%;
+}
+
+#root {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ margin-bottom: 80px;
+}
+
+.logo {
+ margin-bottom: 20px;
+ -webkit-user-drag: none;
+ height: 128px;
+ width: 128px;
+ will-change: filter;
+ transition: filter 300ms;
+}
+
+.logo:hover {
+ filter: drop-shadow(0 0 1.2em #6988e6aa);
+}
+
+.creator {
+ font-size: 14px;
+ line-height: 16px;
+ color: var(--ev-c-text-2);
+ font-weight: 600;
+ margin-bottom: 10px;
+}
+
+.text {
+ font-size: 28px;
+ color: var(--ev-c-text-1);
+ font-weight: 700;
+ line-height: 32px;
+ text-align: center;
+ margin: 0 10px;
+ padding: 16px 0;
+}
+
+.tip {
+ font-size: 16px;
+ line-height: 24px;
+ color: var(--ev-c-text-2);
+ font-weight: 600;
+}
+
+.react {
+ background: -webkit-linear-gradient(315deg, #087ea4 55%, #7c93ee);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-weight: 700;
+}
+
+.ts {
+ background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-weight: 700;
+}
+
+.actions {
+ display: flex;
+ padding-top: 32px;
+ margin: -6px;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+}
+
+.action {
+ flex-shrink: 0;
+ padding: 6px;
+}
+
+.action a {
+ cursor: pointer;
+ text-decoration: none;
+ display: inline-block;
+ border: 1px solid transparent;
+ text-align: center;
+ font-weight: 600;
+ white-space: nowrap;
+ border-radius: 20px;
+ padding: 0 20px;
+ line-height: 38px;
+ font-size: 14px;
+ border-color: var(--ev-button-alt-border);
+ color: var(--ev-button-alt-text);
+ background-color: var(--ev-button-alt-bg);
+}
+
+.action a:hover {
+ border-color: var(--ev-button-alt-hover-border);
+ color: var(--ev-button-alt-hover-text);
+ background-color: var(--ev-button-alt-hover-bg);
+}
+
+.versions {
+ position: absolute;
+ bottom: 30px;
+ margin: 0 auto;
+ padding: 15px 0;
+ font-family: 'Menlo', 'Lucida Console', monospace;
+ display: inline-flex;
+ overflow: hidden;
+ align-items: center;
+ border-radius: 22px;
+ background-color: #202127;
+ backdrop-filter: blur(24px);
+}
+
+.versions li {
+ display: block;
+ float: left;
+ border-right: 1px solid var(--ev-c-gray-1);
+ padding: 0 20px;
+ font-size: 14px;
+ line-height: 14px;
+ opacity: 0.8;
+ &:last-child {
+ border: none;
+ }
+}
+
+@media (max-width: 720px) {
+ .text {
+ font-size: 20px;
+ }
+}
+
+@media (max-width: 620px) {
+ .versions {
+ display: none;
+ }
+}
+
+@media (max-width: 350px) {
+ .tip,
+ .actions {
+ display: none;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/assets/wavy-lines.svg b/apps/agent-tars/src/renderer/src/assets/wavy-lines.svg
new file mode 100644
index 000000000..d08c61199
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/assets/wavy-lines.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/agent-tars/src/renderer/src/components/AgentApp/index.tsx b/apps/agent-tars/src/renderer/src/components/AgentApp/index.tsx
new file mode 100644
index 000000000..e01b7caea
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentApp/index.tsx
@@ -0,0 +1,81 @@
+import { OpenAgentChatUI } from '@renderer/components/ChatUI';
+import { showCanvasAtom } from '@renderer/state/canvas';
+import { motion, AnimatePresence } from 'framer-motion';
+import { useAtom } from 'jotai';
+import { CanvasPanel } from '../CanvasPanel';
+import { LeftSidebar } from '../LeftSidebar';
+import { Toaster } from 'react-hot-toast';
+import { isReportHtmlMode } from '@renderer/constants';
+import { FilePermissionHandler } from '../FilePermissionHandler';
+import { useFileSystemSettings } from '@renderer/hooks/useFileSystemSettings';
+
+export function AgentApp() {
+ const [showCanvas] = useAtom(showCanvasAtom);
+
+ useFileSystemSettings();
+
+ if (isReportHtmlMode) {
+ return (
+
+
+
+
+
+ {showCanvas && (
+
+
+
+ )}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {showCanvas && (
+
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/EventRenderer.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/EventRenderer.tsx
new file mode 100644
index 000000000..29443fbef
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/EventRenderer.tsx
@@ -0,0 +1,35 @@
+import { EventItem, EventType } from '@renderer/type/event';
+import { LoadingStatus } from './events/LoadingStatus';
+import { ToolUsed } from './events/ToolUsed';
+import { AgentStatus } from './events/AgentStatus';
+import { ChatText } from './events/ChatText';
+// import { Observation } from './events/Observation';
+import { UserInteruption } from './events/UserInteruption';
+import { AgentFlowEnd } from './events/End';
+
+export function EventRenderer({
+ event,
+ isLastEvent,
+}: {
+ event: EventItem;
+ isLastEvent: boolean;
+}) {
+ const components = {
+ [EventType.LoadingStatus]: LoadingStatus,
+ [EventType.ToolUsed]: ToolUsed,
+ [EventType.AgentStatus]: AgentStatus,
+ [EventType.ChatText]: ChatText,
+ // [EventType.Observation]: Observation,
+ [EventType.UserInteruption]: UserInteruption,
+ [EventType.End]: AgentFlowEnd,
+ };
+
+ const Component = components[event.type];
+ if (!Component) return null;
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/GroupSection.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/GroupSection.tsx
new file mode 100644
index 000000000..1aef269af
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/GroupSection.tsx
@@ -0,0 +1,61 @@
+import { UIGroup, UIGroupType } from '@renderer/utils/parseEvents';
+import { StepSection } from './StepSection';
+import { ChatText } from './events/ChatText';
+import { PlanTask } from '@renderer/type/agent';
+import { LoadingStatus } from './events/LoadingStatus';
+import { AgentFlowEnd } from './events/End';
+import { EventType } from '@renderer/type/event';
+
+interface GroupSectionProps {
+ group: UIGroup;
+ planTasks: PlanTask[];
+ stepIndex?: number;
+ groups: UIGroup[];
+}
+
+export function GroupSection({
+ group,
+ planTasks,
+ stepIndex,
+ groups,
+}: GroupSectionProps) {
+ if (group.type === UIGroupType.ChatText) {
+ return ;
+ }
+
+ if (group.type === UIGroupType.Loading) {
+ return ;
+ }
+
+ if (group.type === UIGroupType.End) {
+ return ;
+ }
+
+ if (group.type === UIGroupType.PlanStep && stepIndex) {
+ const previousGroups = groups.slice(0, groups.indexOf(group));
+
+ const lastPlanStepGroup = [...previousGroups]
+ .reverse()
+ .find((g) => g.type === UIGroupType.PlanStep);
+
+ const isContinuation = lastPlanStepGroup?.step === stepIndex;
+ const isEnded = groups.find((g) => g.type === UIGroupType.End);
+ const events = groups.flatMap((g) => g.events);
+ const currentStep =
+ events.find((e) => e.type === EventType.NewPlanStep) || 0;
+
+ return (
+
+ );
+ }
+
+ return null;
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/ProgressBar.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/ProgressBar.tsx
new file mode 100644
index 000000000..51e41bb29
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/ProgressBar.tsx
@@ -0,0 +1,38 @@
+import { motion } from 'framer-motion';
+import { cn } from '@renderer/utils';
+
+interface ProgressBarProps {
+ progress: number;
+ className?: string;
+}
+
+export function ProgressBar({ progress, className }: ProgressBarProps) {
+ return (
+
+ {/* Glow effect */}
+
+
+ {/* Progress indicator */}
+
+ {/* Main progress bar */}
+
+ {/* Shine effect */}
+
+
+
+ {/* Glow overlay */}
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/StepSection.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/StepSection.tsx
new file mode 100644
index 000000000..7a1f5bbc3
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/StepSection.tsx
@@ -0,0 +1,131 @@
+import { useState } from 'react';
+import { EventItem } from '@renderer/type/event';
+import { PlanTask, PlanTaskStatus } from '@renderer/type/agent';
+import { CheckCircle, Loader2 } from 'lucide-react';
+import { HiOutlineChevronUpDown } from 'react-icons/hi2';
+import { EventRenderer } from './EventRenderer';
+
+interface StepSectionProps {
+ step: number;
+ events: EventItem[];
+ isCurrentStep: boolean;
+ planTasks: PlanTask[];
+ isContinuation?: boolean;
+}
+
+export function StepSection({
+ step,
+ events,
+ isCurrentStep,
+ planTasks,
+ isContinuation = false,
+}: StepSectionProps) {
+ const [isExpanded, setIsExpanded] = useState(true);
+ const stepInfo = planTasks[step - 1];
+
+ if (!stepInfo) {
+ console.error('Step info not found for step:', step);
+ return null;
+ }
+
+ const { status, title } = stepInfo;
+ const isSingleTask = planTasks.length === 1;
+
+ return (
+
+
!isSingleTask && setIsExpanded(!isExpanded)}
+ >
+
+ {!isSingleTask && (
+
+ Step {step}
+ {isContinuation && (
+
+
+
+
+
+ Continue
+
+ )}
+
+ )}
+ {!isSingleTask && (
+
+ )}
+
+
+
+ {title}
+
+
+ {status === PlanTaskStatus.Done && (
+
+ )}
+ {status === PlanTaskStatus.Doing && (
+
+ )}
+
+
+
+
+
+
+ {events.map((event, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/AgentStatus.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/AgentStatus.tsx
new file mode 100644
index 000000000..ea05f6999
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/AgentStatus.tsx
@@ -0,0 +1,12 @@
+import { MarkdownRenderer } from '@vendor/chat-ui';
+import { EventItem } from '@renderer/type/event';
+
+export function AgentStatus({ event }: { event: EventItem }) {
+ return (
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/ChatText.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/ChatText.tsx
new file mode 100644
index 000000000..413a5cc13
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/ChatText.tsx
@@ -0,0 +1,109 @@
+import { getFileIcon, MarkdownRenderer } from '@vendor/chat-ui';
+import { EventContentDescriptor, EventItem } from '@renderer/type/event';
+import { ErrorBoundary } from '@renderer/components/ErrorBoundary';
+import { FiPaperclip } from 'react-icons/fi';
+import path from 'path-browserify';
+import { useAtom } from 'jotai';
+import { canvasStateManager } from '@renderer/state/canvas';
+import { CanvasType } from '@renderer/type/canvas';
+import { ipcClient } from '@renderer/api';
+import { isReportHtmlMode } from '@renderer/constants';
+
+interface AttachmentItemProps {
+ path: string;
+ onClick: (path: string) => void;
+}
+
+const AttachmentItem = ({ path: filePath, onClick }: AttachmentItemProps) => {
+ const fileName = path.basename(filePath);
+
+ const getIcon = () => {
+ return getFileIcon(filePath);
+ };
+
+ const Icon = getIcon();
+
+ return (
+ onClick(filePath)}
+ className="group flex items-center gap-3 p-3 rounded-lg border border-gray-200
+ dark:border-gray-700 bg-white dark:bg-gray-800
+ hover:bg-gray-50 dark:hover:bg-gray-750
+ cursor-pointer transition-all duration-200"
+ >
+
+
+
+
+ );
+};
+
+export function ChatText({ event }: { event: EventItem }) {
+ const eventContent = event.content as EventContentDescriptor['chat-text'];
+ const [, updateCanvasState] = useAtom(canvasStateManager.updateState);
+
+ const handleAttachmentClick = async (filePath: string) => {
+ let content: string | null = null;
+ if (isReportHtmlMode) {
+ const artifacts = window.__OMEGA_REPORT_DATA__?.artifacts || {};
+ const fileName = filePath.split('/').pop()!;
+ content = artifacts[fileName].content || '';
+ } else {
+ content = await ipcClient.getFileContent({
+ filePath,
+ });
+ }
+
+ if (!content) {
+ return;
+ }
+
+ updateCanvasState({
+ isVisible: true,
+ dataSource: {
+ type: CanvasType.ArtifactPreview,
+ data: {
+ filePath,
+ content: content,
+ },
+ },
+ });
+ };
+
+ return (
+
+ {event.content}
}
+ >
+
+
+ {eventContent.attachments && eventContent.attachments.length > 0 && (
+
+
+
+ Attachments
+
+
+
+ {eventContent.attachments.map((attachment, index) => (
+
+ ))}
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/End.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/End.tsx
new file mode 100644
index 000000000..f6825f89a
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/End.tsx
@@ -0,0 +1,11 @@
+import { MarkdownRenderer } from '@vendor/chat-ui';
+import { EventItem } from '@renderer/type/event';
+
+export function AgentFlowEnd({ event }: { event: EventItem }) {
+ const defaultEndMessage =
+ '> I have finished the tasks. Thank you for your time. Goodbye.';
+
+ return (
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/LoadingStatus.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/LoadingStatus.tsx
new file mode 100644
index 000000000..bcec92efc
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/LoadingStatus.tsx
@@ -0,0 +1,60 @@
+import { EventItem } from '@renderer/type/event';
+import { useEffect, useState } from 'react';
+
+const colors = [
+ 'rgb(239, 68, 68)', // red-500
+ 'rgb(249, 115, 22)', // orange-500
+ 'rgb(234, 179, 8)', // yellow-500
+ 'rgb(34, 197, 94)', // green-500
+ 'rgb(59, 130, 246)', // blue-500
+ 'rgb(168, 85, 247)', // purple-500
+];
+
+export function LoadingStatus({
+ title,
+ event,
+}: {
+ title: string;
+ event?: EventItem;
+}) {
+ const [colorIndex, setColorIndex] = useState(0);
+ const loadingTitle = event?.content.title ?? title;
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setColorIndex((i) => (i + 1) % colors.length);
+ }, 2000);
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+
+
+
+
+ {loadingTitle}
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/Observation.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/Observation.tsx
new file mode 100644
index 000000000..d634a9902
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/Observation.tsx
@@ -0,0 +1,19 @@
+import { EventItem } from '@renderer/type/event';
+
+export function Observation({ event }: { event: EventItem }) {
+ const contentString =
+ typeof event.content === 'object'
+ ? JSON.stringify(event.content, null, 2)
+ : String(event.content);
+
+ return (
+
+
+ Observation
+
+
+ {contentString}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/ToolUsed.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/ToolUsed.tsx
new file mode 100644
index 000000000..f590f5670
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/ToolUsed.tsx
@@ -0,0 +1,144 @@
+import { EventContentDescriptor, EventItem } from '@renderer/type/event';
+import {
+ ActionStatus,
+ ToolPlatform,
+ toolToPlatformMap,
+} from '@renderer/type/agent';
+import { FiCheck, FiX, FiLoader, FiSearch, FiGlobe } from 'react-icons/fi';
+import {
+ FiFolder, // FileSystem
+ FiTerminal, // CommandLine
+ FiSettings, // System
+ FiBox, // Default
+} from 'react-icons/fi';
+import { getLoadingTipFromToolCall } from '@renderer/utils/getLoadingTipForToolCall';
+import { getFileIcon } from '@vendor/chat-ui';
+import { useAtom } from 'jotai';
+import { canvasStateManager } from '@renderer/state/canvas';
+import { CanvasType } from '@renderer/type/canvas';
+import { currentEventIdAtom } from '@renderer/state/chat';
+
+const platformIcons = {
+ [ToolPlatform.FileSystem]: FiFolder,
+ [ToolPlatform.CommandLine]: FiTerminal,
+ [ToolPlatform.System]: FiSettings,
+ [ToolPlatform.Search]: FiSearch,
+ [ToolPlatform.Browser]: FiGlobe,
+ default: FiBox,
+};
+
+const statusIcons = {
+ [ActionStatus.Success]: FiCheck,
+ [ActionStatus.Failed]: FiX,
+ [ActionStatus.Running]: FiLoader,
+};
+
+const statusClasses = {
+ [ActionStatus.Success]: 'text-emerald-500 dark:text-emerald-400',
+ [ActionStatus.Failed]: 'text-rose-500 dark:text-rose-400',
+ [ActionStatus.Running]: 'text-sky-500 dark:text-sky-400 animate-spin',
+};
+
+export function ToolUsed({ event }: { event: EventItem }) {
+ const content = event.content as EventContentDescriptor['tool-used'];
+ const { value = '', description = '' } = getLoadingTipFromToolCall(
+ content.tool,
+ content.params,
+ content.status,
+ );
+ const [, setCurrentEventId] = useAtom(currentEventIdAtom);
+ const platform = toolToPlatformMap[content.tool] || ToolPlatform.System;
+
+ const PlatformIcon =
+ platformIcons[platform] || platformIcons.default || FiBox;
+
+ const StatusIcon =
+ statusIcons[content.status] || statusIcons[ActionStatus.Failed];
+ const statusClass =
+ statusClasses[content.status] || statusClasses[ActionStatus.Failed];
+
+ const [, updateCanvasState] = useAtom(canvasStateManager.updateState);
+
+ const getSymbolIcon = () => {
+ if (
+ platform === ToolPlatform.FileSystem &&
+ content.tool?.includes('file') &&
+ typeof value === 'string'
+ ) {
+ const icon = getFileIcon(value);
+ return (
+ icon || (
+
+ )
+ );
+ }
+
+ return (
+
+ );
+ };
+
+ const handleClick = () => {
+ updateCanvasState({
+ isVisible: true,
+ dataSource: {
+ type: CanvasType.EventPlayer,
+ data: {},
+ },
+ });
+ setCurrentEventId(event.id);
+ };
+
+ return (
+
+
+ {/* Platform Icon */}
+
+ {platform === ToolPlatform.FileSystem ? (
+
+ {getSymbolIcon()}
+
+ ) : (
+
+ )}
+
+
+ {/* Content */}
+
+
+ {description || 'No description'}
+
+
+ {value || 'No value'}
+
+
+
+ {/* Status Icon */}
+
+
+
+
+
+ {/* Hover Effect Overlay */}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/UserInteruption.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/UserInteruption.tsx
new file mode 100644
index 000000000..9382a1474
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/events/UserInteruption.tsx
@@ -0,0 +1,89 @@
+import { motion } from 'framer-motion';
+import { EventItem } from '@renderer/type/event';
+import { AlertCircle, Clock } from 'lucide-react';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+
+dayjs.extend(relativeTime);
+
+export function UserInteruption({
+ event,
+ isLastEvent,
+}: {
+ event: EventItem;
+ isLastEvent: boolean;
+}) {
+ const timestamp = dayjs(event.timestamp);
+
+ return (
+
+
+ {/* Glow Effect */}
+
+
+ {/* Content Container */}
+
+ {/* Header */}
+
+
+
+
+ User Interruption Detected
+
+
+
+
+
+
+ {timestamp.fromNow()}
+
+
+
+
+ {/* Message */}
+
+
+ {(event.content as any).text}
+
+
+ {/* Tech Pattern Background */}
+
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/icons.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/icons.tsx
new file mode 100644
index 000000000..a30b4d780
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/icons.tsx
@@ -0,0 +1,12 @@
+import { FileEdit, Search, Code, Terminal, Globe, Image } from 'lucide-react';
+
+export const toolIcons = {
+ file: FileEdit,
+ editor: FileEdit,
+ search: Search,
+ code: Code,
+ terminal: Terminal,
+ browser: Globe,
+ image: Image,
+ default: Code,
+};
diff --git a/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/index.tsx b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/index.tsx
new file mode 100644
index 000000000..92f1a78e6
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/AgentFlowMessage/index.tsx
@@ -0,0 +1,37 @@
+import { MessageItem, OmegaAgentData } from '@renderer/type/chatMessage';
+import { extractEventStreamUIMeta, UIGroupType } from '../../utils/parseEvents';
+import { GroupSection } from './GroupSection';
+
+export function AgentFlowMessage({ message }: { message: MessageItem }) {
+ const flowDataEvents = (message.content as OmegaAgentData).events || [];
+ const meta = extractEventStreamUIMeta(flowDataEvents);
+
+ if (flowDataEvents.length === 0) {
+ return (
+
+ No events to display
+
+ );
+ }
+
+ return (
+
+
+ {meta.eventGroups.map((group, index) => {
+ const currentStepIndex =
+ group.type === UIGroupType.PlanStep ? group.step : undefined;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/HtmlPreview.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/HtmlPreview.tsx
new file mode 100644
index 000000000..ab979cbd8
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/HtmlPreview.tsx
@@ -0,0 +1,38 @@
+import { useEffect, useRef } from 'react';
+
+interface HtmlPreviewProps {
+ content: string;
+}
+
+export function HtmlPreview({ content }: HtmlPreviewProps) {
+ const iframeRef = useRef(null);
+
+ useEffect(() => {
+ if (iframeRef.current) {
+ // 使用 data URL 来加载 HTML 内容
+ const blob = new Blob([content], { type: 'text/html' });
+ const url = URL.createObjectURL(blob);
+
+ iframeRef.current.src = url;
+
+ // 清理 URL
+ return () => {
+ URL.revokeObjectURL(url);
+ };
+ }
+
+ return () => {};
+ }, [content]);
+
+ return (
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/ImagePreview.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/ImagePreview.tsx
new file mode 100644
index 000000000..8ebf50851
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/ImagePreview.tsx
@@ -0,0 +1,30 @@
+import { useState } from 'react';
+
+interface ImagePreviewProps {
+ src: string;
+ alt: string;
+}
+
+export function ImagePreview({ src, alt }: ImagePreviewProps) {
+ const [isZoomed, setIsZoomed] = useState(false);
+ return (
+
+
setIsZoomed(!isZoomed)}
+ onMouseLeave={() => setIsZoomed(false)}
+ />
+ {isZoomed && (
+
+ Click to zoom out
+
+ )}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/index.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/index.tsx
new file mode 100644
index 000000000..b30dde932
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/index.tsx
@@ -0,0 +1,78 @@
+import { canvasDataSourceAtom } from '@renderer/state/canvas';
+import { MarkdownRenderer } from '@vendor/chat-ui';
+import { useAtom } from 'jotai';
+import { useMemo } from 'react';
+import { HtmlPreview } from './HtmlPreview';
+import { ImagePreview } from './ImagePreview';
+import { getFileExtension } from './utils';
+
+export enum ArtifactType {
+ HTML = 'html',
+ Markdown = 'markdown',
+ Image = 'image',
+ PDF = 'pdf',
+}
+
+export interface ArtifactData {
+ filePath: string;
+ content: string;
+ type: ArtifactType;
+}
+
+export function Artifact() {
+ const [dataSource] = useAtom(canvasDataSourceAtom);
+
+ if (!dataSource) {
+ return null;
+ }
+
+ const { filePath, content, type } = dataSource.data as ArtifactData;
+
+ const fileType = useMemo(() => {
+ if (type) return type;
+ return getFileExtension(filePath);
+ }, [filePath, type]);
+
+ const renderContent = () => {
+ switch (fileType) {
+ case ArtifactType.Markdown:
+ return (
+
+
+
+ );
+ case ArtifactType.HTML:
+ return ;
+ case ArtifactType.Image:
+ return ;
+ case ArtifactType.PDF:
+ return (
+
+ );
+ default:
+ return (
+
+ Unsupported file type
+
+ );
+ }
+ };
+
+ return (
+
+
+ {filePath.split('/').pop()}
+
+
{renderContent()}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/utils.ts b/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/utils.ts
new file mode 100644
index 000000000..239e772fc
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/Artifact/utils.ts
@@ -0,0 +1,24 @@
+import { ArtifactType } from './index';
+
+export function getFileExtension(filePath: string): ArtifactType {
+ const extension = filePath.split('.').pop()?.toLowerCase();
+
+ switch (extension) {
+ case 'md':
+ case 'markdown':
+ return ArtifactType.Markdown;
+ case 'html':
+ case 'htm':
+ return ArtifactType.HTML;
+ case 'jpg':
+ case 'jpeg':
+ case 'png':
+ case 'gif':
+ case 'webp':
+ return ArtifactType.Image;
+ case 'pdf':
+ return ArtifactType.PDF;
+ default:
+ return ArtifactType.Markdown;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/PlayerControls.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/PlayerControls.tsx
new file mode 100644
index 000000000..846d7faf7
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/PlayerControls.tsx
@@ -0,0 +1,105 @@
+import { useCallback, useState } from 'react';
+import { FiSkipBack, FiSkipForward } from 'react-icons/fi';
+
+interface PlayerControlsProps {
+ currentTime: number;
+ startTime: number;
+ endTime: number;
+ onPrevious: () => void;
+ onNext: () => void;
+ onSeek: (timestamp: number) => void;
+}
+
+export function PlayerControls({
+ currentTime,
+ startTime,
+ endTime,
+ onPrevious,
+ onNext,
+ onSeek,
+}: PlayerControlsProps) {
+ const [hoverTime, setHoverTime] = useState(null);
+
+ const formatTime = useCallback(
+ (timestamp: number) => {
+ const duration = Math.floor((timestamp - startTime) / 1000);
+ const minutes = Math.floor(duration / 60);
+ const seconds = duration % 60;
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+ },
+ [startTime],
+ );
+
+ const progress = ((currentTime - startTime) / (endTime - startTime)) * 100;
+
+ const handleSeek = (value: number) => {
+ const timestamp = startTime + ((endTime - startTime) * value) / 100;
+ onSeek(timestamp);
+ };
+
+ const handleMouseMove = (e: React.MouseEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect();
+ const percentage = (e.clientX - rect.left) / rect.width;
+ const timestamp = startTime + (endTime - startTime) * percentage;
+ setHoverTime(timestamp);
+ };
+
+ if (startTime === 0 && endTime === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ 00:00
+
+
setHoverTime(null)}
+ >
+
handleSeek(Number(e.target.value))}
+ className="w-full h-1 bg-gray-200 dark:bg-gray-700 rounded-full appearance-none cursor-pointer"
+ style={{
+ marginTop: '2px', // 微调垂直位置
+ }}
+ />
+ {hoverTime && (
+
+ {formatTime(hoverTime)}
+
+ )}
+
+
+ {formatTime(endTime)}
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/index.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/index.tsx
new file mode 100644
index 000000000..d2358083a
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/index.tsx
@@ -0,0 +1,217 @@
+import { useAtom } from 'jotai';
+import {
+ FiX,
+ FiMonitor,
+ FiCheck,
+ FiLoader,
+ FiAlertCircle,
+ FiInbox,
+} from 'react-icons/fi';
+import { showCanvasAtom, canvasDataSourceAtom } from '@renderer/state/canvas';
+import {
+ currentEventIdAtom,
+ eventsAtom,
+ planTasksAtom,
+} from '@renderer/state/chat';
+import { PlayerControls } from './PlayerControls';
+import { ActionStatus } from '@renderer/type/agent';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import {
+ EventContentDescriptor,
+ EventItem,
+ EventType,
+} from '@renderer/type/event';
+import { renderPlatformPanel } from './renderPlatformPanel';
+
+const StatusIcon = ({ status }: { status: ActionStatus }) => {
+ switch (status) {
+ case ActionStatus.Success:
+ return ;
+ case ActionStatus.Running:
+ return ;
+ case ActionStatus.Failed:
+ return ;
+ default:
+ return null;
+ }
+};
+
+export function EventPlayer() {
+ const [, setShowCanvas] = useAtom(showCanvasAtom);
+ const [dataSource] = useAtom(canvasDataSourceAtom);
+ const [events] = useAtom(eventsAtom);
+ const [currentEvent, setCurrentEvent] = useState();
+ const [, setPlanTasks] = useAtom(planTasksAtom);
+ const [eventId] = useAtom(currentEventIdAtom);
+
+ const toolEvents = useMemo(() => {
+ return events
+ .filter((event) => event.type === EventType.ToolUsed)
+ .sort((a, b) => a.timestamp - b.timestamp);
+ }, [events]);
+
+ const timeRange = useMemo(() => {
+ if (toolEvents.length === 0) {
+ return { start: 0, end: 0 };
+ }
+ return {
+ start: toolEvents[0].timestamp,
+ end: toolEvents[toolEvents.length - 1].timestamp,
+ };
+ }, [toolEvents]);
+
+ const findEventByTimestamp = useCallback(
+ (timestamp: number) => {
+ return toolEvents.reduce(
+ (prev, curr) => {
+ if (!prev) return curr;
+ return Math.abs(curr.timestamp - timestamp) <
+ Math.abs(prev.timestamp - timestamp)
+ ? curr
+ : prev;
+ },
+ undefined as EventItem | undefined,
+ );
+ },
+ [toolEvents],
+ );
+
+ const handlePrevious = useCallback(() => {
+ if (!currentEvent || toolEvents.length === 0) return;
+ const currentIndex = toolEvents.findIndex((e) => e.id === currentEvent.id);
+ if (currentIndex > 0) {
+ setCurrentEvent(toolEvents[currentIndex - 1]);
+ }
+ }, [currentEvent, toolEvents]);
+
+ const handleNext = useCallback(() => {
+ if (!currentEvent || toolEvents.length === 0) return;
+ const currentIndex = toolEvents.findIndex((e) => e.id === currentEvent.id);
+ if (currentIndex < toolEvents.length - 1) {
+ setCurrentEvent(toolEvents[currentIndex + 1]);
+ }
+ }, [currentEvent, toolEvents]);
+
+ const handleSeek = useCallback(
+ (timestamp: number) => {
+ const event = findEventByTimestamp(timestamp);
+ const currentEventIndex = toolEvents.findIndex((e) => e.id === event?.id);
+ const lastPlanUpdateEvent = toolEvents
+ .slice(0, currentEventIndex)
+ .reverse()
+ .find((e) => e.type === EventType.PlanUpdate);
+ setPlanTasks(lastPlanUpdateEvent?.content?.planTasks || []);
+ setCurrentEvent(event);
+ },
+ [findEventByTimestamp],
+ );
+
+ useEffect(() => {
+ const event = toolEvents.find((item) => item.id === eventId);
+ setCurrentEvent(event || toolEvents[0]);
+ }, [dataSource, toolEvents]);
+
+ const eventData =
+ currentEvent?.content as EventContentDescriptor['tool-used'];
+
+ const renderContent = () => {
+ if (!currentEvent || toolEvents.length === 0) {
+ return (
+
+
+
+
+ No events to display
+
+
+ Please select an event to view its details and execution progress
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {eventData?.tool}
+
+
+ {eventData?.value}
+
+
+ {eventData?.status && (
+
+ )}
+
+
+ {currentEvent && renderPlatformPanel({ event: currentEvent })}
+
+
+ );
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ My Computer
+
+ setShowCanvas(false)}
+ className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
+ >
+
+
+
+
+ {/* Tool Info */}
+
+
+
+ {eventData?.description || 'Tool execution details'}
+
+
+
+ {/* Main Content Area */}
+
{renderContent()}
+
+ {/* Player Controls */}
+
+
+ {/* Progress Bar */}
+ {/*
*/}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/IconMapping.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/IconMapping.tsx
new file mode 100644
index 000000000..28d8170b5
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/IconMapping.tsx
@@ -0,0 +1,40 @@
+import { ToolCallType } from '@renderer/type/agent';
+import {
+ FiGlobe,
+ FiCamera,
+ FiMousePointer,
+ FiType,
+ FiList,
+ FiCode,
+ FiArrowDown,
+ FiColumns,
+} from 'react-icons/fi';
+
+export function getIconForTool(tool: ToolCallType) {
+ switch (tool) {
+ case ToolCallType.BrowserNavigate:
+ return ;
+ case ToolCallType.BrowserScreenshot:
+ return ;
+ case ToolCallType.BrowserClick:
+ case ToolCallType.BrowserHover:
+ return ;
+ case ToolCallType.BrowserFormInputFill:
+ case ToolCallType.BrowserSelect:
+ return ;
+ case ToolCallType.BrowserGetHtml:
+ case ToolCallType.BrowserGetText:
+ case ToolCallType.BrowserReadLinks:
+ return ;
+ case ToolCallType.BrowserEvaluate:
+ return ;
+ case ToolCallType.BrowserScroll:
+ return ; // 替换 FiScroll
+ case ToolCallType.BrowserNewTab:
+ case ToolCallType.BrowserCloseTab:
+ case ToolCallType.BrowserSwitchTab:
+ return ;
+ default:
+ return ;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/DefaultContent.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/DefaultContent.tsx
new file mode 100644
index 000000000..90f6c0170
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/DefaultContent.tsx
@@ -0,0 +1,39 @@
+import { ContentProps } from '../types';
+import { getIconForTool } from '../IconMapping';
+
+export function DefaultContent({ tool, result }: ContentProps) {
+ const getScreenshotPath = () => {
+ if (Array.isArray(result)) {
+ const screenshot = result.find((item) => item.type === 'image');
+ return screenshot?.path
+ ? `file://${screenshot.path}`
+ : screenshot?.content;
+ }
+ return null;
+ };
+
+ const screenshotPath = getScreenshotPath();
+ return (
+
+
+
+ {getIconForTool(tool)}
+
+ {String(tool)
+ .replace(/^browser_/, '')
+ .replace(/_/g, ' ')}
+
+
+ {screenshotPath && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/LinksContent.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/LinksContent.tsx
new file mode 100644
index 000000000..557b304d7
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/LinksContent.tsx
@@ -0,0 +1,32 @@
+import { FiGlobe } from 'react-icons/fi';
+import { ContentProps } from '../types';
+import { DefaultTip } from '../../DefaultTip';
+
+export function LinksContent({ result }: ContentProps) {
+ if (!Array.isArray(result)) {
+ return ;
+ }
+
+ return (
+
+
+ {result.map((link, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/ScreenshotContent.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/ScreenshotContent.tsx
new file mode 100644
index 000000000..f343f6af1
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/ScreenshotContent.tsx
@@ -0,0 +1,22 @@
+import { ContentProps } from '../types';
+import { DefaultTip } from '../../DefaultTip';
+
+export function ScreenshotContent({ params, result }: ContentProps) {
+ if (!result) {
+ return ;
+ }
+
+ return (
+
+
+
+ Screenshot: {params.name}
+ {params.selector && ({params.selector}) }
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/TextContent.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/TextContent.tsx
new file mode 100644
index 000000000..2b5215b3c
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/components/TextContent.tsx
@@ -0,0 +1,21 @@
+import { ContentProps } from '../types';
+import { MonacoEditor } from '@renderer/components/MonacoEditor';
+
+export function TextContent({ result }: ContentProps) {
+ const content =
+ result
+ ?.map((item: { type: string; text: string }) => item.text)
+ .join('\n') || '';
+
+ return (
+
+ {content ? (
+
+ ) : (
+
+ Loading content...
+
+ )}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/index.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/index.tsx
new file mode 100644
index 000000000..a91e25b42
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/index.tsx
@@ -0,0 +1,27 @@
+import { ToolCallType } from '@renderer/type/agent';
+import { BrowserPanelProps } from './types';
+import { ScreenshotContent } from './components/ScreenshotContent';
+import { TextContent } from './components/TextContent';
+import { LinksContent } from './components/LinksContent';
+import { DefaultContent } from './components/DefaultContent';
+
+export function BrowserPanel({ tool, params, result }: BrowserPanelProps) {
+ const toolType = tool as ToolCallType;
+ const contentProps = { tool: toolType, params, result };
+
+ const renderContent = () => {
+ switch (toolType) {
+ case ToolCallType.BrowserScreenshot:
+ return ;
+ case ToolCallType.BrowserGetHtml:
+ case ToolCallType.BrowserGetText:
+ return ;
+ case ToolCallType.BrowserReadLinks:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return {renderContent()}
;
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/types.ts b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/types.ts
new file mode 100644
index 000000000..542e0184f
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/BrowserPanel/types.ts
@@ -0,0 +1,13 @@
+import { ToolCallType } from '@renderer/type/agent';
+
+export interface BrowserPanelProps {
+ tool: string;
+ params: any;
+ result?: any;
+}
+
+export interface ContentProps {
+ tool: ToolCallType;
+ params: any;
+ result?: any;
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/DefaultTip.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/DefaultTip.tsx
new file mode 100644
index 000000000..be45c7480
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/DefaultTip.tsx
@@ -0,0 +1,17 @@
+import { ActionStatus } from '@renderer/type/agent';
+import { FiCheckCircle } from 'react-icons/fi';
+
+export function DefaultTip({
+ description,
+}: {
+ description?: string;
+ status?: ActionStatus;
+}) {
+ return (
+
+
+
Operation completed successfully
+
{description}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/FsPanel/FileEditPreview.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/FsPanel/FileEditPreview.tsx
new file mode 100644
index 000000000..fdb879380
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/FsPanel/FileEditPreview.tsx
@@ -0,0 +1,151 @@
+import { useState } from 'react';
+import { MonacoEditor } from '@renderer/components/MonacoEditor';
+// @ts-ignore
+import { extractLanguage } from './index';
+
+interface FileEditPreviewProps {
+ original: string;
+ path: string;
+ edits:
+ | Array<{
+ oldText: string;
+ newText: string;
+ }>
+ | undefined;
+ content: string;
+}
+
+function normalizeLineEndings(text: string): string {
+ return text.replace(/\r\n/g, '\n');
+}
+
+function applyEdits(
+ content: string,
+ edits: FileEditPreviewProps['edits'],
+): string {
+ let modifiedContent = normalizeLineEndings(content);
+
+ for (const edit of edits || []) {
+ const normalizedOld = normalizeLineEndings(edit.oldText);
+ const normalizedNew = normalizeLineEndings(edit.newText);
+
+ if (modifiedContent.includes(normalizedOld)) {
+ modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
+ continue;
+ }
+
+ // Try line-by-line matching
+ const oldLines = normalizedOld.split('\n');
+ const contentLines = modifiedContent.split('\n');
+
+ for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
+ const potentialMatch = contentLines.slice(i, i + oldLines.length);
+ const isMatch = oldLines.every(
+ (oldLine, j) => oldLine.trim() === potentialMatch[j].trim(),
+ );
+
+ if (isMatch) {
+ const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
+ const newLines = normalizedNew.split('\n').map((line, j) => {
+ if (j === 0) return originalIndent + line.trimStart();
+ const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
+ const newIndent = line.match(/^\s*/)?.[0] || '';
+ if (oldIndent && newIndent) {
+ const relativeIndent = newIndent.length - oldIndent.length;
+ return (
+ originalIndent +
+ ' '.repeat(Math.max(0, relativeIndent)) +
+ line.trimStart()
+ );
+ }
+ return line;
+ });
+
+ contentLines.splice(i, oldLines.length, ...newLines);
+ modifiedContent = contentLines.join('\n');
+ break;
+ }
+ }
+ }
+
+ return modifiedContent;
+}
+
+export function FileEditPreview({
+ original,
+ path,
+ edits,
+ content,
+}: FileEditPreviewProps) {
+ const [activeTab, setActiveTab] = useState<'original' | 'diff' | 'new'>(
+ 'original',
+ );
+
+ const newContent = edits ? applyEdits(original, edits) : content;
+
+ const tabs = [
+ { id: 'original', label: 'Original' },
+ { id: 'diff', label: 'Diff' },
+ { id: 'new', label: 'New' },
+ ] as const;
+
+ return (
+
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id)}
+ className={`
+
+ group relative px-4 py-2.5 text-sm font-medium transition-all duration-200
+ ${
+ activeTab === tab.id
+ ? 'text-primary-600 dark:text-primary-400'
+ : 'text-gray-500 dark:text-gray-400 hover:text-primary-500 dark:hover:text-primary-300'
+ }
+ `}
+ >
+ {tab.label}
+
+
+ ))}
+
+
+
+ {activeTab === 'original' && (
+
+ )}
+ {activeTab === 'diff' && (
+
+ )}
+ {activeTab === 'new' && (
+
+ )}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/FsPanel/index.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/FsPanel/index.tsx
new file mode 100644
index 000000000..5eb350368
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/FsPanel/index.tsx
@@ -0,0 +1,72 @@
+import { MonacoEditor } from '@renderer/components/MonacoEditor';
+// @ts-ignore
+import { FileEditPreview } from './FileEditPreview';
+import { ToolCallType } from '@renderer/type/agent';
+import { Folder } from 'lucide-react';
+
+interface FsPanelProps {
+ toolName: string;
+ path: string;
+ content: string;
+ original?: string;
+ edits?: Array<{
+ oldText: string;
+ newText: string;
+ }>;
+}
+
+export const extractLanguage = (path: string) => {
+ const extension =
+ path
+ ?.split('.')
+ ?.pop()
+ ?.replace(/\s*[\(\(][^)]*[\)\)]\s*$/, '')
+ .split('.')
+ .pop() || 'plaintext';
+ return extension;
+};
+
+export function FsPanel({
+ path,
+ content,
+ original,
+ edits,
+ toolName,
+}: FsPanelProps) {
+ if (toolName === ToolCallType.ListAllowedDirectories) {
+ const [label, ...dirs] = content.split('\n');
+ return (
+
+
{label}
+
+ {dirs.map((dir, index) => (
+
+
+ {dir}
+
+ ))}
+
+
+ );
+ }
+
+ if (original) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/SearchPanel.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/SearchPanel.tsx
new file mode 100644
index 000000000..977092be5
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/SearchPanel.tsx
@@ -0,0 +1,67 @@
+import { SearchResult } from '@agent-infra/search';
+import { FiExternalLink, FiSearch } from 'react-icons/fi';
+
+interface SearchPanelProps {
+ query: string;
+ result: SearchResult;
+}
+
+export function SearchPanel({
+ query,
+ result = { pages: [] },
+}: SearchPanelProps) {
+ return (
+
+ {/* Search Query Section */}
+
+
+
+ Search results for: "{query}"
+
+
+
+ {/* Results List */}
+
+ {(result.pages || []).map((page, index) => (
+
+ ))}
+
+
+ {(result.pages || []).length === 0 && (
+
+ )}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/TerminalPanel.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/TerminalPanel.tsx
new file mode 100644
index 000000000..57d09735d
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/platform/TerminalPanel.tsx
@@ -0,0 +1,130 @@
+import { useThemeMode } from '@renderer/hooks/useThemeMode';
+import { useEffect, useRef } from 'react';
+import { Terminal } from 'xterm';
+import { FitAddon } from 'xterm-addon-fit';
+import { WebLinksAddon } from 'xterm-addon-web-links';
+import 'xterm/css/xterm.css';
+
+interface TerminalPanelProps {
+ command: string;
+ result?: any;
+}
+
+export function TerminalPanel({ command, result }: TerminalPanelProps) {
+ const terminalRef = useRef(null);
+ const xtermRef = useRef(null);
+ const isDarkMode = useThemeMode();
+
+ useEffect(() => {
+ if (!terminalRef.current) return;
+
+ const lightTheme = {
+ background: '#ffffff',
+ foreground: '#2e2e2e',
+ cursor: '#333333',
+ selectionBackground: '#b3d7ff',
+ black: '#2e2e2e',
+ brightBlack: '#666666',
+ red: '#e34747',
+ brightRed: '#f14c4c',
+ green: '#09a16d',
+ brightGreen: '#23d18b',
+ yellow: '#c5a332',
+ brightYellow: '#f5f543',
+ blue: '#0098dd',
+ brightBlue: '#3b8eea',
+ magenta: '#bc3fbc',
+ brightMagenta: '#d670d6',
+ cyan: '#11a8cd',
+ brightCyan: '#29b8db',
+ white: '#e5e5e5',
+ brightWhite: '#ffffff',
+ };
+
+ const darkTheme = {
+ background: '#1e1e1e',
+ foreground: '#d4d4d4',
+ cursor: '#ffffff',
+ selectionBackground: '#264f78',
+ black: '#000000',
+ brightBlack: '#666666',
+ red: '#cd3131',
+ brightRed: '#f14c4c',
+ green: '#0dbc79',
+ brightGreen: '#23d18b',
+ yellow: '#e5e510',
+ brightYellow: '#f5f543',
+ blue: '#2472c8',
+ brightBlue: '#3b8eea',
+ magenta: '#bc3fbc',
+ brightMagenta: '#d670d6',
+ cyan: '#11a8cd',
+ brightCyan: '#29b8db',
+ white: '#e5e5e5',
+ brightWhite: '#ffffff',
+ };
+
+ const term = new Terminal({
+ theme: isDarkMode.value ? darkTheme : lightTheme,
+ fontFamily: 'Menlo, Monaco, "Courier New", monospace',
+ fontSize: 13,
+ lineHeight: 1.4,
+ cursorBlink: true,
+ cursorStyle: 'block',
+ scrollback: 5000,
+ // padding: 8,
+ // rendererType: 'canvas',
+ allowTransparency: true,
+ });
+
+ const fitAddon = new FitAddon();
+ term.loadAddon(fitAddon);
+ term.loadAddon(new WebLinksAddon());
+
+ xtermRef.current = term;
+ term.open(terminalRef.current);
+ fitAddon.fit();
+
+ const updateTerminalSize = () => {
+ if (terminalRef.current && xtermRef.current) {
+ const { offsetWidth, offsetHeight } = terminalRef.current;
+ const cols = Math.floor(offsetWidth / 9);
+ const rows = Math.floor(offsetHeight / 17);
+ xtermRef.current.resize(cols, rows);
+ fitAddon.fit();
+ }
+ };
+
+ const resizeObserver = new ResizeObserver(updateTerminalSize);
+ resizeObserver.observe(terminalRef.current);
+
+ term.writeln('\x1b[32m$\x1b[0m ' + command);
+
+ if (result) {
+ result.forEach((item) => {
+ term.writeln(item.text);
+ });
+ }
+
+ const resizeHandler = () => fitAddon.fit();
+ window.addEventListener('resize', resizeHandler);
+
+ return () => {
+ resizeObserver.disconnect();
+ window.removeEventListener('resize', resizeHandler);
+ term.dispose();
+ };
+ }, [command, result, isDarkMode.value]);
+
+ return (
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/renderPlatformPanel.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/renderPlatformPanel.tsx
new file mode 100644
index 000000000..668c13e24
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/EventPlayer/renderPlatformPanel.tsx
@@ -0,0 +1,87 @@
+import { ToolPlatform, toolToPlatformMap } from '@renderer/type/agent';
+import { TerminalPanel } from './platform/TerminalPanel';
+import { SearchPanel } from './platform/SearchPanel';
+import { EventContentDescriptor, EventItem } from '@renderer/type/event';
+import { SearchResult } from '@agent-infra/search';
+import { BrowserPanel } from './platform/BrowserPanel';
+// @ts-ignore
+import { FsPanel } from './platform/FSPanel';
+
+export interface PanelDataForPlatform {
+ [ToolPlatform.CommandLine]: {
+ command: string;
+ result: string;
+ };
+ [ToolPlatform.FileSystem]: {
+ toolName: string;
+ path: string;
+ content: string;
+ original?: string;
+ edits?: Array<{
+ oldText: string;
+ newText: string;
+ }>;
+ };
+ [ToolPlatform.Search]: {
+ query: string;
+ result: SearchResult;
+ };
+ [ToolPlatform.Browser]: {
+ tool: string;
+ params: any;
+ result?: any;
+ };
+}
+
+export function renderPlatformPanel({ event }: { event: EventItem }) {
+ const data = event.content as EventContentDescriptor['tool-used'];
+ const platform = toolToPlatformMap[data.tool];
+ let platformData;
+
+ switch (platform) {
+ case ToolPlatform.CommandLine:
+ platformData = {
+ command: data.value,
+ result: data.result,
+ };
+ break;
+ case ToolPlatform.FileSystem:
+ const toolCallParam = JSON.parse(data.params || '{}') || {};
+ platformData = {
+ toolName: data.tool,
+ path: data.value,
+ original: data.original,
+ edits: toolCallParam.edits,
+ content: toolCallParam.content || data.result?.[0].text,
+ } as PanelDataForPlatform[ToolPlatform.FileSystem];
+ break;
+ case ToolPlatform.Search:
+ platformData = {
+ query: data.value,
+ result: data.result,
+ } as PanelDataForPlatform[ToolPlatform.Search];
+ break;
+ case ToolPlatform.Browser:
+ platformData = {
+ tool: data.tool,
+ params: JSON.parse(data.params || '{}'),
+ result: data.result,
+ } as PanelDataForPlatform[ToolPlatform.Browser];
+ break;
+ default:
+ return null;
+ }
+
+ switch (platform) {
+ case ToolPlatform.FileSystem:
+ return ;
+ case ToolPlatform.CommandLine:
+ return ;
+ case ToolPlatform.Search:
+ return ;
+ case ToolPlatform.Browser:
+ return ;
+ default:
+ return null;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/components/CanvasPanel/index.tsx b/apps/agent-tars/src/renderer/src/components/CanvasPanel/index.tsx
new file mode 100644
index 000000000..6a9bc8621
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/CanvasPanel/index.tsx
@@ -0,0 +1,20 @@
+import { useAtom } from 'jotai';
+import { EventPlayer } from './EventPlayer';
+import { canvasDataSourceAtom } from '@renderer/state/canvas';
+import { CanvasType } from '@renderer/type/canvas';
+import { Artifact } from './Artifact';
+
+export function CanvasPanel() {
+ const [dataSource] = useAtom(canvasDataSourceAtom);
+
+ if (!dataSource) return ;
+
+ switch (dataSource.type) {
+ case CanvasType.EventPlayer:
+ return ;
+ case CanvasType.ArtifactPreview:
+ return ;
+ default:
+ return null;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/AgentStatusTip/index.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/AgentStatusTip/index.tsx
new file mode 100644
index 000000000..61aded543
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/AgentStatusTip/index.tsx
@@ -0,0 +1,33 @@
+import { agentStatusTipAtom } from '@renderer/state/chat';
+import { motion, AnimatePresence } from 'framer-motion';
+import { useAtom } from 'jotai';
+import { RiSparklingFill } from 'react-icons/ri';
+
+export function AgentStatusTip() {
+ const [currentStatusTip] = useAtom(agentStatusTipAtom);
+
+ return (
+
+
+
+
+
+
+ {currentStatusTip}
+
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/BeforeInputContainer.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/BeforeInputContainer.tsx
new file mode 100644
index 000000000..9532c4c9b
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/BeforeInputContainer.tsx
@@ -0,0 +1,29 @@
+import { useAppChat } from '@renderer/hooks/useAppChat';
+import { UserInteruptArea } from './UserInteruptArea';
+import { useThemeMode } from '@renderer/hooks/useThemeMode';
+import { isReportHtmlMode } from '@renderer/constants';
+import { PlanTaskStatus } from './PlanTaskStatus';
+import { Replay } from './Replay';
+// import { PlanTaskStatus } from './PlanTaskStatus';
+
+export function BeforeInputContainer() {
+ const { messageSending } = useAppChat();
+ const isDarkMode = useThemeMode();
+ return (
+
+ {messageSending ? (
+
+ ) : null}
+ {isReportHtmlMode ?
: null}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/MenuHeader.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/MenuHeader.tsx
new file mode 100644
index 000000000..efdccda21
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/MenuHeader.tsx
@@ -0,0 +1,133 @@
+import Logo from '../../assets/logo.png';
+import { IoShareSocialOutline } from 'react-icons/io5';
+import {
+ HiOutlineChevronDoubleLeft,
+ HiOutlineChevronDoubleRight,
+} from 'react-icons/hi';
+import { useAtom } from 'jotai';
+import { showCanvasAtom } from '@renderer/state/canvas';
+import { isReportHtmlMode } from '@renderer/constants';
+import { useAppChat } from '@renderer/hooks/useAppChat';
+import { useDisclosure } from '@nextui-org/react';
+import { ShareModal } from './ShareModal';
+import { motion } from 'framer-motion';
+import { useState } from 'react';
+import { useChatSessions } from '@renderer/hooks/useChatSession';
+
+export function MenuHeader() {
+ const [showCanvas, setShowCanvas] = useAtom(showCanvasAtom);
+ const { messages } = useAppChat();
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [isShareHovered, setIsShareHovered] = useState(false);
+ const [isPanelHovered, setIsPanelHovered] = useState(false);
+ const { chatSessions, currentSessionId } = useChatSessions({
+ appId: 'omega-agent',
+ });
+ const currentSession = chatSessions.find(
+ (session) => session.id === currentSessionId,
+ );
+ return (
+
+
+
+ {/* Logo */}
+
+
+
+
+
+ {isReportHtmlMode ? (
+
+
+ {currentSession?.name || 'New Session'}
+
+
+ ) : null}
+
+
+ {!isReportHtmlMode && (
+ setIsShareHovered(true)}
+ onMouseLeave={() => setIsShareHovered(false)}
+ onClick={onOpen}
+ className="p-2 rounded-xl bg-background hover:bg-primary/5 border border-divider hover:border-primary/30 transition-all duration-200 relative group"
+ title="Share"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+
+ Share
+
+
+ )}
+
+ setIsPanelHovered(true)}
+ onMouseLeave={() => setIsPanelHovered(false)}
+ onClick={() => setShowCanvas(!showCanvas)}
+ className="p-2 rounded-xl bg-background hover:bg-primary/5 border border-divider hover:border-primary/30 transition-all duration-200 relative group"
+ title={showCanvas ? 'Hide Panel' : 'Show Panel'}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+ {showCanvas ? (
+
+ ) : (
+
+ )}
+
+
+ {showCanvas ? 'Hide' : 'Show'}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/PlanTaskStatus/index.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/PlanTaskStatus/index.tsx
new file mode 100644
index 000000000..4627ff9b2
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/PlanTaskStatus/index.tsx
@@ -0,0 +1,102 @@
+import { planTasksAtom } from '@renderer/state/chat';
+import { useAtom } from 'jotai';
+import { useState } from 'react';
+import { PlanTaskStatus as TaskStatus } from '@renderer/type/agent';
+import { Popover, PopoverTrigger, PopoverContent } from '@nextui-org/react';
+import { motion } from 'framer-motion';
+import { FiClock, FiCheck, FiX } from 'react-icons/fi';
+
+export function PlanTaskStatus() {
+ const [planTasks] = useAtom(planTasksAtom);
+ const [isOpen, setIsOpen] = useState(false);
+ const completedTasks =
+ planTasks?.filter((task) => task.status === TaskStatus.Done).length || 0;
+
+ const getStatusIcon = (status: TaskStatus) => {
+ switch (status) {
+ case TaskStatus.Done:
+ return ;
+ case TaskStatus.Doing:
+ return ;
+ case TaskStatus.Skipped:
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ if (!planTasks?.length) return null;
+
+ return (
+
+
+
+
+
+
+
+ Plan Steps
+
+
+
+ {completedTasks}/{planTasks.length} completed
+
+
+
+
+
+
+
+
+
+ Plan Progress
+
+
+
+ {planTasks.map((task, index) => (
+
+
+
+ {getStatusIcon(task.status)}
+
+
+
+ {task.title}
+
+ {task.error && (
+
+ {task.error}
+
+ )}
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/Replay.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/Replay.tsx
new file mode 100644
index 000000000..5bf5585ff
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/Replay.tsx
@@ -0,0 +1,201 @@
+import { useAppChat } from '@renderer/hooks/useAppChat';
+import { currentEventIdAtom, eventsAtom } from '@renderer/state/chat';
+import { MessageItem, MessageType } from '@renderer/type/chatMessage';
+import { EventItem, EventType } from '@renderer/type/event';
+import { ChatMessageUtil } from '@renderer/utils/ChatMessageUtils';
+import { atom, useAtom } from 'jotai';
+import { useState, useEffect, useRef } from 'react';
+import { FiRotateCw, FiPause, FiPlay } from 'react-icons/fi';
+import { isReportHtmlMode } from '@renderer/constants';
+
+type ButtonState = 'replay' | 'pause' | 'continue';
+
+interface ButtonConfig {
+ icon: React.ReactElement;
+ label: string;
+ style: string;
+}
+
+const BUTTON_CONFIGS: Record = {
+ replay: {
+ icon: ,
+ label: 'Replay',
+ style: 'bg-blue-500 hover:bg-blue-600 text-white',
+ },
+ pause: {
+ icon: ,
+ label: 'Pause',
+ style: 'bg-red-500 hover:bg-red-600 text-white',
+ },
+ continue: {
+ icon: ,
+ label: 'Continue',
+ style: 'bg-blue-500 hover:bg-blue-600 text-white',
+ },
+};
+
+const replayAllMessages = atom([]);
+
+// wait 3s to replay
+const DEFAULT_COUNTDOWN = 3;
+
+export function Replay() {
+ const [allMessages, setAllMessages] = useAtom(replayAllMessages);
+ const [, setEvents] = useAtom(eventsAtom);
+ const [, setEventId] = useAtom(currentEventIdAtom);
+ const { addMessage, updateMessage, setMessages, messageEndRef, messages } =
+ useAppChat();
+ const timerRef = useRef();
+ const intervalRef = useRef();
+ const [buttonState, setButtonState] = useState('replay');
+ const [countdown, setCountdown] = useState(DEFAULT_COUNTDOWN);
+ const playbackRef = useRef<{
+ currentIndex: number;
+ eventIndex: number;
+ }>({ currentIndex: 0, eventIndex: 0 });
+
+ const clearPlayTimer = () => {
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ timerRef.current = undefined;
+ }
+ };
+
+ const clearCountDownInterval = () => {
+ setCountdown(0);
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = undefined;
+ }
+ };
+
+ useEffect(() => {
+ return () => {
+ clearPlayTimer();
+ clearCountDownInterval();
+ };
+ }, []);
+
+ useEffect(() => {
+ if (isReportHtmlMode && allMessages.length) {
+ intervalRef.current = setInterval(() => {
+ setCountdown((prevCountdown) => {
+ if (prevCountdown > 1) {
+ return prevCountdown - 1;
+ } else {
+ clearCountDownInterval();
+ handleTogglePlay();
+ return 0;
+ }
+ });
+ }, 1000);
+ }
+ }, [allMessages]);
+
+ useEffect(() => {
+ if (allMessages.length === 0 && messages.length !== 0) {
+ setAllMessages(messages);
+ }
+ }, [messages.length]);
+
+ const startPlayback = () => {
+ setButtonState('pause');
+ if (buttonState === 'replay') {
+ setMessages([]);
+ playbackRef.current = { currentIndex: 0, eventIndex: 0 };
+ }
+
+ timerRef.current = setInterval(async () => {
+ const { currentIndex, eventIndex } = playbackRef.current;
+ const message = allMessages[currentIndex];
+
+ if (!message) {
+ clearPlayTimer();
+ setButtonState('replay');
+ playbackRef.current = { currentIndex: 0, eventIndex: 0 };
+ return;
+ }
+
+ if (message.type === MessageType.OmegaAgent) {
+ const messageContent = message.content as { events: EventItem[] };
+ const events = messageContent.events;
+
+ if (eventIndex === 0) {
+ await addMessage(
+ ChatMessageUtil.assistantOmegaMessage({
+ events: [],
+ }),
+ );
+ }
+
+ if (eventIndex >= events.length) {
+ playbackRef.current.currentIndex++;
+ playbackRef.current.eventIndex = 0;
+ } else {
+ const currentEvents = events.slice(0, eventIndex + 1);
+ await updateMessage(
+ {
+ ...message,
+ content: { events: currentEvents },
+ },
+ {
+ shouldSyncStorage: false,
+ },
+ );
+ messageEndRef.current?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ inline: 'nearest',
+ });
+ playbackRef.current.eventIndex++;
+ setEvents(currentEvents);
+ const currentToolUseEvent = [...currentEvents]
+ .reverse()
+ .find((e) => e.type === EventType.ToolUsed);
+ if (currentToolUseEvent) setEventId(currentToolUseEvent.id);
+ }
+ } else {
+ await addMessage(message, {
+ shouldSyncStorage: false,
+ });
+ playbackRef.current.currentIndex++;
+ }
+ }, 100);
+ };
+
+ const handleTogglePlay = () => {
+ clearCountDownInterval();
+ switch (buttonState) {
+ case 'replay':
+ case 'continue':
+ startPlayback();
+ break;
+ case 'pause':
+ clearPlayTimer();
+ setButtonState('continue');
+ break;
+ }
+ };
+
+ const currentConfig = BUTTON_CONFIGS[buttonState];
+
+ return (
+
+
+ {currentConfig.icon}
+ {currentConfig.label}
+
+ {isReportHtmlMode && countdown > 0 && (
+
+ start replay in {countdown} seconds
+
+ )}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/ShareModal.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/ShareModal.tsx
new file mode 100644
index 000000000..78aaa2192
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/ShareModal.tsx
@@ -0,0 +1,153 @@
+import {
+ Modal,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ Button,
+ Input,
+ Tab,
+ Tabs,
+ Link,
+ Tooltip,
+} from '@nextui-org/react';
+import { useState, useEffect } from 'react';
+import { ipcClient } from '@renderer/api';
+
+const API_URL_STORAGE_KEY = 'report-api-url';
+
+interface ShareModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ messages: any[];
+}
+
+export function ShareModal({ isOpen, onClose, messages }: ShareModalProps) {
+ const [apiUrl, setApiUrl] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [resultUrl, setResultUrl] = useState('');
+ const [error, setError] = useState('');
+
+ // Load saved API URL from localStorage when component mounts
+ useEffect(() => {
+ const savedApiUrl = localStorage.getItem(API_URL_STORAGE_KEY);
+ if (savedApiUrl) {
+ setApiUrl(savedApiUrl);
+ }
+ }, []);
+
+ const handleShare = async (type: 'local' | 'remote') => {
+ try {
+ setIsLoading(true);
+ setError('');
+
+ const url = await ipcClient.saveReportHtml({
+ messages,
+ reportApiUrl: type === 'remote' ? apiUrl : undefined,
+ });
+
+ // Save API URL if remote upload is successful
+ if (type === 'remote' && url) {
+ localStorage.setItem(API_URL_STORAGE_KEY, apiUrl);
+ }
+
+ setResultUrl(url);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Share failed');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ Share Conversation
+
+
+
+
+ handleShare('local')}
+ isLoading={isLoading}
+ >
+ Save to Local
+
+
+
+
+
+
+
+ API URL
+
+ API Requirements:
+
+ Accept POST requests
+ Request body as multipart/form-data
+ File field name: 'file'
+
+ Response format:{' '}
+ {'{ "url": "file access URL" }'}
+
+
+
+ }
+ >
+
+ (?)
+
+
+
+ }
+ placeholder="Enter upload API URL"
+ value={apiUrl}
+ onChange={(e) => setApiUrl(e.target.value)}
+ description={
+ localStorage.getItem(API_URL_STORAGE_KEY)
+ ? 'Last used API URL auto-filled'
+ : 'API URL will be saved after successful upload'
+ }
+ />
+
+ handleShare('remote')}
+ isLoading={isLoading}
+ isDisabled={!apiUrl}
+ >
+ Upload
+
+
+
+
+
+ {error && {error}
}
+
+ {resultUrl && (
+
+
Share Link:
+
+ {resultUrl}
+
+
+ )}
+
+
+
+ Close
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/StatusBar.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/StatusBar.tsx
new file mode 100644
index 000000000..56f63667f
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/StatusBar.tsx
@@ -0,0 +1,76 @@
+import { Loader2 } from 'lucide-react';
+import { ProgressBar } from '../AgentFlowMessage/ProgressBar';
+import {
+ EventStreamUIMeta,
+ extractEventStreamUIMeta,
+} from '@renderer/utils/parseEvents';
+import { PlanTaskStatus } from '@renderer/type/agent';
+import { useEffect, useState } from 'react';
+import { useAtomValue } from 'jotai';
+import { eventsAtom } from '@renderer/state/chat';
+
+export function StatusBar() {
+ const [uiMeta, setUIMeta] = useState(null);
+ const events = useAtomValue(eventsAtom);
+ useEffect(() => {
+ if (events.length > 0) {
+ const meta = extractEventStreamUIMeta(events);
+ if (meta.planTasks.length > 0) {
+ setUIMeta(meta);
+ }
+ }
+ }, [events]);
+
+ if (!uiMeta) {
+ return null;
+ }
+
+ const { currentStepIndex, planTasks } = uiMeta;
+
+ const lastFinishedTask = [...planTasks]
+ .reverse()
+ .find((task) => task.status === PlanTaskStatus.Done);
+
+ const progress = (currentStepIndex / Math.max(planTasks.length, 1)) * 100;
+
+ const isLoading = planTasks.some(
+ (task) => task.status === PlanTaskStatus.Doing,
+ );
+
+ const getStatusText = () => {
+ if (isLoading) {
+ const currentTask = planTasks[currentStepIndex - 1];
+ return currentTask?.title || 'Processing...';
+ }
+ return lastFinishedTask?.title;
+ };
+
+ return (
+
+ {/* Glass morphism background */}
+
+
+ {/* Content */}
+
+
+
+ {isLoading && (
+
+ )}
+
+ {getStatusText()}
+
+
+
+ Step {currentStepIndex} of {planTasks.length}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/UserInteruptArea/index.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/UserInteruptArea/index.tsx
new file mode 100644
index 000000000..bb8230662
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/UserInteruptArea/index.tsx
@@ -0,0 +1,195 @@
+import { useState } from 'react';
+import {
+ currentAgentFlowIdRefAtom,
+ globalEventEmitter,
+} from '@renderer/state/chat';
+import { useAtom } from 'jotai';
+import styled, { keyframes } from 'styled-components';
+import { Send } from 'lucide-react';
+
+interface UserInteruptAreaProps {
+ isDark?: boolean;
+ onSubmit?: (value: string) => void;
+}
+
+const flowAnimation = keyframes`
+ 0% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+ 100% {
+ background-position: 0% 50%;
+ }
+`;
+
+const FlowContainer = styled.div<{ isDark: boolean }>`
+ position: relative;
+ width: 100%;
+ padding: 2px;
+ border-radius: 16px;
+ background: linear-gradient(
+ 90deg,
+ rgba(160, 124, 254, 0.8),
+ rgba(254, 143, 181, 0.8),
+ rgba(1, 111, 238, 0.8),
+ rgba(160, 124, 254, 0.8)
+ );
+ background-size: 300% 300%;
+ animation: ${flowAnimation} 8s linear infinite;
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 1px;
+ border-radius: 15px;
+ background: ${(props) => (props.isDark ? '#1a1a1a' : '#ffffff')};
+ z-index: 0;
+ }
+`;
+
+const ContentWrapper = styled.div`
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ padding-left: 16px;
+ padding-right: 8px;
+`;
+
+const StyledInput = styled.input<{ isDark: boolean }>`
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: ${(props) => (props.isDark ? '#ffffff' : '#000000')};
+ font-size: 0.95rem;
+ padding: 2px 0;
+ &::placeholder {
+ color: ${(props) => (props.isDark ? '#666666' : '#999999')};
+ }
+`;
+
+const StyledButton = styled.button<{ isDark: boolean; disabled?: boolean }>`
+ position: relative;
+ background: ${(props) =>
+ props.isDark
+ ? 'linear-gradient(135deg, rgba(1, 111, 238, 0.9), rgba(1, 86, 208, 0.9))'
+ : 'linear-gradient(135deg, rgba(1, 119, 255, 0.9), rgba(1, 111, 238, 0.9))'};
+ color: white;
+ border: none;
+ padding: 8px;
+ margin: 2px;
+ width: 30px;
+ height: 30px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
+ opacity: ${(props) => (props.disabled ? 0.6 : 1)};
+ backdrop-filter: blur(10px);
+ box-shadow: 0 2px 8px
+ ${(props) =>
+ props.isDark ? 'rgba(1, 111, 238, 0.2)' : 'rgba(1, 111, 238, 0.15)'};
+
+ svg {
+ width: 18px;
+ height: 18px;
+ transition: transform 0.3s ease;
+ }
+
+ &:hover:not(:disabled) {
+ transform: translateY(-2px) scale(1.02);
+ box-shadow: 0 4px 16px
+ ${(props) =>
+ props.isDark ? 'rgba(1, 111, 238, 0.4)' : 'rgba(1, 111, 238, 0.25)'};
+ background: ${(props) =>
+ props.isDark
+ ? 'linear-gradient(135deg, rgba(1, 119, 255, 0.95), rgba(1, 86, 208, 0.95))'
+ : 'linear-gradient(135deg, rgba(1, 127, 255, 0.95), rgba(1, 111, 238, 0.95))'};
+
+ svg {
+ transform: translate(1px, -1px);
+ }
+ }
+
+ &:active {
+ transform: translateY(0) scale(0.98);
+ box-shadow: 0 2px 4px
+ ${(props) =>
+ props.isDark ? 'rgba(1, 111, 238, 0.3)' : 'rgba(1, 111, 238, 0.2)'};
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: -1px;
+ border-radius: 14px;
+ background: linear-gradient(
+ 135deg,
+ rgba(255, 255, 255, 0.1),
+ transparent 50%
+ );
+ pointer-events: none;
+ }
+`;
+
+export function UserInteruptArea({
+ isDark = true,
+ onSubmit,
+}: UserInteruptAreaProps) {
+ const [input, setInput] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [currentAgentFlowIdRef] = useAtom(currentAgentFlowIdRefAtom);
+
+ const handleSubmit = async () => {
+ if (input.trim() && currentAgentFlowIdRef.current && !isSubmitting) {
+ setIsSubmitting(true);
+ try {
+ onSubmit?.(input);
+ globalEventEmitter.emit(currentAgentFlowIdRef.current, {
+ type: 'user-interrupt',
+ text: input.trim(),
+ });
+ setInput('');
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit();
+ }
+ };
+
+ return (
+
+
+
+ setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Insert new instructions in the process..."
+ isDark={isDark}
+ disabled={isSubmitting}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/index.scss b/apps/agent-tars/src/renderer/src/components/ChatUI/index.scss
new file mode 100644
index 000000000..ebe1bce59
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/index.scss
@@ -0,0 +1,18 @@
+.ai-app-chat-container {
+ height: 100vh !important;
+ background-color: var(--ai-color-bg-primary) !important;
+ transition: width 0.3s ease !important;
+}
+
+.ai-app-input-section-container {
+ background-color: var(--ai-color-bg-tertiary) !important;
+ border-radius: var(--radius-lg) !important;
+}
+
+.ai-app-chat-messages {
+ margin-top: 16px !important;
+}
+
+.ai-app-input-section-wrapper {
+ padding-top: 15px !important;
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/index.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/index.tsx
new file mode 100644
index 000000000..b1b261430
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/index.tsx
@@ -0,0 +1,154 @@
+import { ChatUI as BaseChatUI, InputFile } from '@vendor/chat-ui';
+import './index.scss';
+import { MenuHeader } from './MenuHeader';
+import { isReportHtmlMode, STORAGE_DB_NAME } from '@renderer/constants';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useAddUserMessage } from '@renderer/hooks/useAddUserMessage';
+import { useAgentFlow } from '@renderer/hooks/useAgentFlow';
+import { renderMessageUI } from './renderMessageUI';
+import { MessageItem } from '@renderer/type/chatMessage';
+import { useThemeMode } from '@renderer/hooks/useThemeMode';
+import { useAtom, useAtomValue } from 'jotai';
+import {
+ currentAgentFlowIdRefAtom,
+ eventsAtom,
+ globalEventEmitter,
+} from '@renderer/state/chat';
+import { BeforeInputContainer } from './BeforeInputContainer';
+import { AgentStatusTip } from './AgentStatusTip';
+import { useAppChat } from '@renderer/hooks/useAppChat';
+import { extractHistoryEvents } from '@renderer/utils/extractHistoryEvents';
+import { useChatSessions } from '@renderer/hooks/useChatSession';
+import { DEFAULT_APP_ID } from '../LeftSidebar';
+
+declare global {
+ interface Window {
+ __OMEGA_REPORT_DATA__?: {
+ messages: MessageItem[];
+ artifacts: {
+ [key: string]: {
+ content: string;
+ };
+ };
+ };
+ }
+}
+
+export function OpenAgentChatUI() {
+ const [isSending, setIsSending] = useState(false);
+ const addUserMessage = useAddUserMessage();
+ const launchAgentFlow = useAgentFlow();
+ const chatUIRef = useRef<{
+ getInputTextArea: () => HTMLTextAreaElement | null;
+ triggerCommand: (command: string) => void;
+ focusInput: () => void;
+ }>(null);
+ const isDarkMode = useThemeMode();
+ const { initMessages, setMessages } = useAppChat();
+ const [, setEvents] = useAtom(eventsAtom);
+ const currentAgentFlowIdRef = useAtomValue(currentAgentFlowIdRefAtom);
+ const { currentSessionId } = useChatSessions({
+ appId: DEFAULT_APP_ID,
+ });
+ const sendMessage = useCallback(
+ async (inputText: string, inputFiles: InputFile[]) => {
+ try {
+ const inputEle = chatUIRef.current?.getInputTextArea();
+ if (inputEle) {
+ inputEle.disabled = true;
+ inputEle.style.cursor = 'not-allowed';
+ }
+ setIsSending(true);
+ await addUserMessage(inputText, inputFiles);
+ // Launch!
+ await launchAgentFlow(inputText, inputFiles);
+ } finally {
+ setIsSending(false);
+ const inputEle = chatUIRef.current?.getInputTextArea();
+ if (inputEle) {
+ inputEle.disabled = false;
+ inputEle.style.cursor = 'auto';
+ }
+ }
+ },
+ [addUserMessage, launchAgentFlow],
+ );
+
+ useEffect(() => {
+ async function init() {
+ const messages =
+ window.__OMEGA_REPORT_DATA__?.messages ?? (await initMessages());
+ setMessages(messages || []);
+ const events = extractHistoryEvents(messages as unknown as MessageItem[]);
+ setEvents(events);
+ }
+ init();
+ }, [currentSessionId]);
+
+ return (
+ <>
+ {
+ return renderMessageUI({
+ message: message as unknown as MessageItem,
+ });
+ }}
+ isDark={isDarkMode.value}
+ onMessageSend={sendMessage}
+ storageDbName={STORAGE_DB_NAME}
+ features={{
+ clearConversationHistory: true,
+ uploadFiles: true,
+ }}
+ onMessageAbort={() => {
+ setIsSending(false);
+ const inputEle = chatUIRef.current?.getInputTextArea();
+ if (inputEle) {
+ inputEle.disabled = false;
+ inputEle.style.cursor = 'auto';
+ }
+ if (currentAgentFlowIdRef.current) {
+ globalEventEmitter.emit(currentAgentFlowIdRef.current, {
+ type: 'terminate',
+ });
+ }
+ }}
+ onClearConversationHistory={() => {
+ setEvents([]);
+ }}
+ slots={{
+ beforeMessageList: ,
+ beforeInputContainer: ,
+ customFeatures: (
+ <>
+
+ {isSending ?
: null}
+ {/*
*/}
+
+ >
+ ),
+ }}
+ classNames={{
+ messageList: 'scrollbar',
+ }}
+ conversationId={currentSessionId || 'default'}
+ inputPlaceholder={
+ isSending
+ ? 'Agent is working, please wait...'
+ : 'Type your message here...'
+ }
+ />
+ >
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ChatUI/renderMessageUI.tsx b/apps/agent-tars/src/renderer/src/components/ChatUI/renderMessageUI.tsx
new file mode 100644
index 000000000..d3c8f2abd
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ChatUI/renderMessageUI.tsx
@@ -0,0 +1,20 @@
+import { MarkdownRenderer } from '@vendor/chat-ui';
+import { MessageItem, MessageType } from '@renderer/type/chatMessage';
+import { AgentFlowMessage } from '../AgentFlowMessage';
+import { MessageRole } from '@vendor/chat-ui';
+
+export function renderMessageUI({ message }: { message: MessageItem }) {
+ switch (message.type) {
+ case MessageType.PlainText:
+ return (
+
+ );
+ case MessageType.OmegaAgent:
+ return ;
+ default:
+ return null;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/components/ErrorBoundary.tsx b/apps/agent-tars/src/renderer/src/components/ErrorBoundary.tsx
new file mode 100644
index 000000000..3748d221a
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/ErrorBoundary.tsx
@@ -0,0 +1,37 @@
+import { Component, ErrorInfo, ReactNode } from 'react';
+
+interface Props {
+ children: ReactNode;
+ fallback?: ReactNode;
+}
+
+interface State {
+ hasError: boolean;
+}
+
+export class ErrorBoundary extends Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(_: Error): State {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error('Error caught by ErrorBoundary:', error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+ this.props.fallback || (
+ Rendering error
+ )
+ );
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/components/FilePermissionHandler/index.tsx b/apps/agent-tars/src/renderer/src/components/FilePermissionHandler/index.tsx
new file mode 100644
index 000000000..3a6cd5a98
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/FilePermissionHandler/index.tsx
@@ -0,0 +1,14 @@
+import { useAtom } from 'jotai';
+import { pendingPermissionRequestAtom } from '@renderer/services/filePermissionService';
+import { FilePermissionModal } from '../FilePermissionModal';
+
+export function FilePermissionHandler() {
+ const [pendingRequest] = useAtom(pendingPermissionRequestAtom);
+
+ return (
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/FilePermissionModal/index.tsx b/apps/agent-tars/src/renderer/src/components/FilePermissionModal/index.tsx
new file mode 100644
index 000000000..f082c8aa9
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/FilePermissionModal/index.tsx
@@ -0,0 +1,121 @@
+import { useState } from 'react';
+import {
+ Modal,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ Button,
+ Spinner,
+} from '@nextui-org/react';
+import { IoWarningOutline } from 'react-icons/io5';
+import { ipcClient } from '@renderer/api';
+import {
+ loadFileSystemSettings,
+ saveFileSystemSettings,
+} from '@renderer/services/fileSystemSettings';
+import path from 'path-browserify';
+import { resolvePermission } from '@renderer/services/filePermissionService';
+import { useAppSettings } from '../LeftSidebar/Settings/useAppSettings';
+
+interface FilePermissionModalProps {
+ isOpen: boolean;
+ filePath: string;
+}
+
+export function FilePermissionModal({
+ isOpen,
+ filePath,
+}: FilePermissionModalProps) {
+ const [isProcessing, setIsProcessing] = useState(false);
+ const { setSettings } = useAppSettings();
+ const directoryPath = path.dirname(filePath);
+
+ const handleAllow = async () => {
+ setIsProcessing(true);
+ try {
+ // Add this directory to allowed directories
+ const settings = loadFileSystemSettings() || {
+ availableDirectories: [],
+ };
+ if (!settings.availableDirectories.includes(directoryPath)) {
+ settings.availableDirectories.push(directoryPath);
+ saveFileSystemSettings(settings);
+ setSettings((appSettings) => {
+ return {
+ ...appSettings,
+ fileSystem: {
+ ...appSettings.fileSystem,
+ availableDirectories: settings.availableDirectories,
+ },
+ };
+ });
+ await ipcClient.updateFileSystemConfig(settings);
+ }
+
+ // Resolve the permission promise with true (allowed)
+ resolvePermission(true);
+ } catch (error) {
+ console.error('Failed to update file system permissions:', error);
+ resolvePermission(false);
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ const handleDeny = () => {
+ // Resolve the permission promise with false (denied)
+ resolvePermission(false);
+ };
+
+ return (
+
+
+ {() => (
+ <>
+
+
+ Permission Required
+
+
+
+ The application is trying to access a file outside of allowed
+ directories:
+
+
+ {filePath}
+
+
+ Do you want to allow access to this directory and add it to the
+ list of allowed directories?
+
+
+
+
+ Deny
+
+ : null}
+ >
+ {isProcessing ? 'Processing...' : 'Allow'}
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/BottomBar.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/BottomBar.tsx
new file mode 100644
index 000000000..b31e1cef1
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/BottomBar.tsx
@@ -0,0 +1,22 @@
+import { FiSettings } from 'react-icons/fi';
+import { Tooltip } from '@nextui-org/react';
+import styles from './index.module.scss';
+
+interface BottomBarProps {
+ isCollapsed: boolean;
+ onOpenSettings: () => void;
+}
+
+export function BottomBar({ isCollapsed, onOpenSettings }: BottomBarProps) {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/DeleteModal.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/DeleteModal.tsx
new file mode 100644
index 000000000..89060a903
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/DeleteModal.tsx
@@ -0,0 +1,42 @@
+import {
+ Button,
+ Modal,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+} from '@nextui-org/react';
+
+interface DeleteModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+}
+
+export function DeleteModal({ isOpen, onClose, onConfirm }: DeleteModalProps) {
+ return (
+
+
+ {(onModalClose) => (
+ <>
+ Confirm
+
+
+ Are you sure to delete this session? This operation is
+ irreversible.
+
+
+
+
+ Cancel
+
+
+ Delete
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/SessionItem.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/SessionItem.tsx
new file mode 100644
index 000000000..41d2aaec1
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/SessionItem.tsx
@@ -0,0 +1,107 @@
+import { BiCheck, BiEdit } from 'react-icons/bi';
+import { BsThreeDotsVertical, BsTrash } from 'react-icons/bs';
+import {
+ Dropdown,
+ DropdownTrigger,
+ DropdownMenu,
+ DropdownItem,
+} from '@nextui-org/react';
+import styles from './index.module.scss';
+
+interface SessionItemProps {
+ session: {
+ id: string;
+ name: string;
+ };
+ isActive: boolean;
+ isEditing: boolean;
+ editingName: string;
+ onEditingNameChange: (value: string) => void;
+ onKeyDown: (e: React.KeyboardEvent, sessionId: string) => void;
+ onSaveEdit: (sessionId: string, e: React.MouseEvent) => void;
+ onEditSession: (sessionId: string, name: string, e: React.MouseEvent) => void;
+ onDeleteSession: (sessionId: string, e: React.MouseEvent) => void;
+ onClick: () => void;
+}
+
+export function SessionItem({
+ session,
+ isActive,
+ isEditing,
+ editingName,
+ onEditingNameChange,
+ onKeyDown,
+ onSaveEdit,
+ onEditSession,
+ onDeleteSession,
+ onClick,
+}: SessionItemProps) {
+ return (
+
+ {isEditing ? (
+
e.stopPropagation()}
+ >
+ onEditingNameChange(e.target.value)}
+ onKeyDown={(e) => onKeyDown(e, session.id)}
+ autoFocus
+ />
+ onSaveEdit(session.id, e)}
+ >
+
+
+
+ ) : (
+
+ {session.name}
+
+
+ e.stopPropagation()}
+ aria-label="More options"
+ >
+
+
+
+ {
+ if (key === 'edit') {
+ onEditSession(
+ session.id,
+ session.name,
+ new MouseEvent('click') as any,
+ );
+ } else if (key === 'delete') {
+ onDeleteSession(session.id, new MouseEvent('click') as any);
+ }
+ }}
+ >
+ }>
+ Edit
+
+ }
+ >
+ Delete
+
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/SessionList/index.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/SessionList/index.tsx
new file mode 100644
index 000000000..89e44f8e7
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/SessionList/index.tsx
@@ -0,0 +1,114 @@
+import { useMemo } from 'react';
+import {
+ isToday,
+ isYesterday,
+ isThisWeek,
+ isThisMonth,
+ subWeeks,
+ isSameWeek,
+} from 'date-fns';
+import { SessionItem } from '../SessionItem';
+import { ChatSession } from '../type';
+
+interface SessionListProps {
+ sessions: ChatSession[];
+ currentSessionId: string | null;
+ editingSessionId: string | null;
+ editingName: string;
+ onEditingNameChange: (value: string) => void;
+ onKeyDown: (e: React.KeyboardEvent, sessionId: string) => void;
+ onSaveEdit: (sessionId: string, e: React.MouseEvent) => void;
+ onEditSession: (
+ sessionId: string,
+ currentName: string,
+ e: React.MouseEvent,
+ ) => void;
+ onDeleteSession: (sessionId: string, e: React.MouseEvent) => void;
+ onClick: (sessionId: string) => void;
+}
+
+interface GroupedSessions {
+ title: string;
+ sessions: ChatSession[];
+}
+
+export function SessionList({
+ sessions,
+ currentSessionId,
+ editingSessionId,
+ editingName,
+ onEditingNameChange,
+ onKeyDown,
+ onSaveEdit,
+ onEditSession,
+ onDeleteSession,
+ onClick,
+}: SessionListProps) {
+ const groupedSessions = useMemo(() => {
+ const groups: GroupedSessions[] = [
+ { title: 'Today', sessions: [] },
+ { title: 'Yesterday', sessions: [] },
+ { title: 'This Week', sessions: [] },
+ { title: 'Last Week', sessions: [] },
+ { title: 'This Month', sessions: [] },
+ { title: 'Earlier', sessions: [] },
+ ];
+
+ const lastWeekStart = subWeeks(new Date(), 1);
+
+ sessions.forEach((session) => {
+ const date = new Date(session.updatedAt!);
+
+ if (isToday(date)) {
+ groups[0].sessions.push(session);
+ } else if (isYesterday(date)) {
+ groups[1].sessions.push(session);
+ } else if (isThisWeek(date)) {
+ groups[2].sessions.push(session);
+ } else if (isSameWeek(date, lastWeekStart)) {
+ groups[3].sessions.push(session);
+ } else if (isThisMonth(date)) {
+ groups[4].sessions.push(session);
+ } else {
+ groups[5].sessions.push(session);
+ }
+ });
+
+ return groups.filter((group) => group.sessions.length > 0);
+ }, [sessions]);
+
+ if (sessions.length === 0) {
+ return (
+
+ No sessions
+
+ );
+ }
+
+ return (
+
+ {groupedSessions.map((group) => (
+
+
+ {group.title}
+
+ {group.sessions.map((session) => (
+
onClick(session.id)}
+ />
+ ))}
+
+ ))}
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/FileSystemSettingsTab.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/FileSystemSettingsTab.tsx
new file mode 100644
index 000000000..113685a28
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/FileSystemSettingsTab.tsx
@@ -0,0 +1,83 @@
+import { useState } from 'react';
+import { Button, Chip, Input } from '@nextui-org/react';
+import { IoMdAdd } from 'react-icons/io';
+import { FileSystemSettings } from '@agent-infra/shared';
+
+interface FileSystemSettingsTabProps {
+ settings: FileSystemSettings;
+ setSettings: (settings: FileSystemSettings) => void;
+}
+
+export function FileSystemSettingsTab({
+ settings,
+ setSettings,
+}: FileSystemSettingsTabProps) {
+ const [newDirectory, setNewDirectory] = useState('');
+
+ const addDirectory = () => {
+ if (!newDirectory || settings.availableDirectories.includes(newDirectory)) {
+ return;
+ }
+
+ setSettings({
+ ...settings,
+ availableDirectories: [...settings.availableDirectories, newDirectory],
+ });
+ setNewDirectory('');
+ };
+
+ const removeDirectory = (dir: string) => {
+ setSettings({
+ ...settings,
+ availableDirectories: settings.availableDirectories.filter(
+ (d) => d !== dir,
+ ),
+ });
+ };
+
+ return (
+
+
+ Configure directories that the application can access. The default is
+ your home directory's .omega folder.
+
+
+
+ setNewDirectory(e.target.value)}
+ className="flex-1"
+ />
+
+
+
+
+
+
+ {settings.availableDirectories.map((dir) => (
+
removeDirectory(dir)}
+ variant="flat"
+ className="text-xs"
+ >
+ {dir}
+
+ ))}
+ {settings.availableDirectories.length === 0 && (
+
+ No custom directories added. Using default directory.
+
+ )}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/ModelSettingsTab.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/ModelSettingsTab.tsx
new file mode 100644
index 000000000..7d346f299
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/ModelSettingsTab.tsx
@@ -0,0 +1,121 @@
+import { Divider, Input, Select, SelectItem, Spinner } from '@nextui-org/react';
+import { ModelSettings, ModelProvider } from '@agent-infra/shared';
+import { getProviderLogo, getModelOptions } from './modelUtils';
+import { useProviders } from './useProviders';
+
+interface ModelSettingsTabProps {
+ settings: ModelSettings;
+ setSettings: (settings: ModelSettings) => void;
+}
+
+export function ModelSettingsTab({
+ settings,
+ setSettings,
+}: ModelSettingsTabProps) {
+ const { providers, loading } = useProviders();
+ const isAzure = settings.provider === ModelProvider.AZURE_OPENAI;
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
{
+ const provider = e.target.value as ModelProvider;
+ setSettings({
+ ...settings,
+ provider,
+ });
+ }}
+ startContent={getProviderLogo(settings.provider)}
+ >
+ {providers.map((provider) => (
+
+ {provider.charAt(0).toUpperCase() +
+ provider.slice(1).replace('_', ' ')}
+
+ ))}
+
+
+ {isAzure ? (
+
+ setSettings({
+ ...settings,
+ model: e.target.value,
+ })
+ }
+ description="The deployment name of your Azure OpenAI model"
+ isRequired
+ />
+ ) : (
+
setSettings({ ...settings, model: e.target.value })}
+ >
+ {getModelOptions(settings.provider).map((model) => (
+
+ {model.label}
+
+ ))}
+
+ )}
+
+
setSettings({ ...settings, apiKey: e.target.value })}
+ isRequired
+ />
+
+
+
Advanced Settings (Optional)
+
+
+ setSettings({ ...settings, apiVersion: e.target.value })
+ }
+ description={
+ isAzure
+ ? 'Required for Azure OpenAI (e.g., 2023-05-15)'
+ : 'Required for some providers'
+ }
+ isRequired={false}
+ />
+
+
setSettings({ ...settings, endpoint: e.target.value })}
+ description={
+ isAzure
+ ? 'Your Azure OpenAI resource endpoint'
+ : 'Override the default API endpoint'
+ }
+ isRequired={false}
+ />
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/SearchSettingsTab.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/SearchSettingsTab.tsx
new file mode 100644
index 000000000..6bf403ad3
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/SearchSettingsTab.tsx
@@ -0,0 +1,57 @@
+import { Input, Select, SelectItem, Divider } from '@nextui-org/react';
+import { SearchSettings, SearchProvider } from '@agent-infra/shared';
+import { getSearchProviderLogo } from './searchUtils';
+
+interface SearchSettingsTabProps {
+ settings: SearchSettings;
+ setSettings: (settings: SearchSettings) => void;
+}
+
+export function SearchSettingsTab({
+ settings,
+ setSettings,
+}: SearchSettingsTabProps) {
+ return (
+
+
{
+ setSettings({
+ ...settings,
+ provider: e.target.value as SearchProvider,
+ });
+ }}
+ startContent={getSearchProviderLogo(settings.provider)}
+ >
+
+ Bing Search
+
+
+
+
setSettings({ ...settings, apiKey: e.target.value })}
+ isRequired
+ description="Your Bing Search API key"
+ />
+
+
+
Advanced Settings (Optional)
+
+
setSettings({ ...settings, baseUrl: e.target.value })}
+ description="Override the default Bing Search API endpoint"
+ />
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/SettingsModal.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/SettingsModal.tsx
new file mode 100644
index 000000000..59697cee0
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/SettingsModal.tsx
@@ -0,0 +1,94 @@
+import {
+ Modal,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ Button,
+ Tabs,
+ Tab,
+ Spinner,
+} from '@nextui-org/react';
+import { useState } from 'react';
+import { ModelSettingsTab } from './ModelSettingsTab';
+import { FileSystemSettingsTab } from './FileSystemSettingsTab';
+import { SearchSettingsTab } from './SearchSettingsTab';
+import { useAppSettings } from './useAppSettings';
+
+interface SettingsModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
+ const { settings, setSettings, saveSettings } = useAppSettings();
+ const [isSaving, setIsSaving] = useState(false);
+
+ const handleSave = async () => {
+ setIsSaving(true);
+ try {
+ await saveSettings();
+ } catch (error) {
+ console.error('Failed to save settings:', error);
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+
+
+ {(onModalClose) => (
+ <>
+ Settings
+
+
+
+
+ setSettings({ ...settings, model: modelSettings })
+ }
+ />
+
+
+
+ setSettings({ ...settings, search: searchSettings })
+ }
+ />
+
+
+
+ setSettings({ ...settings, fileSystem: fsSettings })
+ }
+ />
+
+
+
+
+
+ Cancel
+
+ : null}
+ >
+ {isSaving ? 'Saving...' : 'Save'}
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/index.ts b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/index.ts
new file mode 100644
index 000000000..d3958a16e
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/index.ts
@@ -0,0 +1 @@
+export * from './SettingsModal';
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/modelUtils.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/modelUtils.tsx
new file mode 100644
index 000000000..5cb78b038
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/modelUtils.tsx
@@ -0,0 +1,57 @@
+import {
+ SiGooglegemini,
+ SiOpenai,
+ SiAnthropic,
+ SiMicrosoftazure,
+} from 'react-icons/si';
+import { AiFillApi } from 'react-icons/ai';
+import MistralIcon from '@renderer/assets/Mistral';
+import { ModelProvider } from '@agent-infra/shared';
+
+export function getProviderLogo(provider: ModelProvider) {
+ switch (provider) {
+ case ModelProvider.OPENAI:
+ return ;
+ case ModelProvider.ANTHROPIC:
+ return ;
+ case ModelProvider.GEMINI:
+ return ;
+ case ModelProvider.MISTRAL:
+ return ;
+ case ModelProvider.AZURE_OPENAI:
+ return ;
+ default:
+ return ;
+ }
+}
+
+export function getModelOptions(provider: ModelProvider) {
+ switch (provider) {
+ case ModelProvider.OPENAI:
+ return [
+ { value: 'gpt-4o', label: 'GPT-4o' },
+ { value: 'gpt-4o-mini', label: 'GPT-4o Mini' },
+ ];
+ case ModelProvider.ANTHROPIC:
+ return [
+ { value: 'claude-3.7-sonnet-latest', label: 'Claude 3.7 Sonnet' },
+ { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet' },
+ ];
+ case ModelProvider.GEMINI:
+ return [
+ { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
+ { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' },
+ { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' },
+ ];
+ case ModelProvider.MISTRAL:
+ return [
+ { value: 'mistral-large-latest', label: 'Mistral Large' },
+ { value: 'mistral-medium-latest', label: 'Mistral Medium' },
+ { value: 'mistral-small-latest', label: 'Mistral Small' },
+ ];
+ case ModelProvider.AZURE_OPENAI:
+ return [];
+ default:
+ return [];
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/searchUtils.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/searchUtils.tsx
new file mode 100644
index 000000000..75260fba4
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/searchUtils.tsx
@@ -0,0 +1,11 @@
+import { SiMicrosoftbing } from 'react-icons/si';
+import { SearchProvider } from '@agent-infra/shared';
+
+export function getSearchProviderLogo(provider: SearchProvider) {
+ switch (provider) {
+ case SearchProvider.BING_SEARCH:
+ return ;
+ default:
+ return null;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/useAppSettings.ts b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/useAppSettings.ts
new file mode 100644
index 000000000..e993d80a4
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/useAppSettings.ts
@@ -0,0 +1,146 @@
+import { useEffect } from 'react';
+import {
+ AppSettings,
+ FileSystemSettings,
+ ModelSettings,
+ ModelProvider,
+ SearchSettings,
+ SearchProvider,
+} from '@agent-infra/shared';
+import {
+ loadLLMSettings,
+ saveLLMSettings,
+} from '../../../services/llmSettings';
+import { updateLLMConfig } from '../../../api/llmConfig';
+import {
+ loadFileSystemSettings,
+ saveFileSystemSettings,
+} from '../../../services/fileSystemSettings';
+import { ipcClient } from '@renderer/api';
+import { atom, useAtom } from 'jotai';
+import toast from 'react-hot-toast';
+import {
+ loadSearchSettings,
+ saveSearchSettings,
+} from '../../../services/searchSettings';
+
+const DEFAULT_MODEL_SETTINGS: ModelSettings = {
+ provider: ModelProvider.OPENAI,
+ model: 'gpt-4o',
+ apiKey: '',
+ apiVersion: '',
+ endpoint: '',
+};
+
+const DEFAULT_FILESYSTEM_SETTINGS: FileSystemSettings = {
+ availableDirectories: [],
+};
+
+const DEFAULT_SEARCH_SETTINGS: SearchSettings = {
+ provider: SearchProvider.BING_SEARCH,
+ apiKey: '',
+};
+
+export const appSettingsAtom = atom({
+ model: DEFAULT_MODEL_SETTINGS,
+ fileSystem: DEFAULT_FILESYSTEM_SETTINGS,
+ search: DEFAULT_SEARCH_SETTINGS,
+});
+
+export function useAppSettings() {
+ const [settings, setSettings] = useAtom(appSettingsAtom);
+
+ // Load settings from localStorage on mount
+ useEffect(() => {
+ const savedModelSettings = loadLLMSettings();
+ const savedFileSystemSettings = loadFileSystemSettings();
+ const savedSearchSettings = loadSearchSettings();
+
+ setSettings({
+ model: savedModelSettings || DEFAULT_MODEL_SETTINGS,
+ fileSystem: savedFileSystemSettings || DEFAULT_FILESYSTEM_SETTINGS,
+ search: savedSearchSettings || DEFAULT_SEARCH_SETTINGS,
+ });
+ }, []);
+
+ const validateModelSettings = (
+ modelSettings: ModelSettings,
+ ): string | null => {
+ if (!modelSettings.provider) {
+ return 'Provider is required';
+ }
+ if (!modelSettings.model) {
+ return 'Model is required';
+ }
+ if (!modelSettings.apiKey) {
+ return 'API Key is required';
+ }
+
+ // Azure OpenAI specific validations
+ if (modelSettings.provider === ModelProvider.AZURE_OPENAI) {
+ if (modelSettings.endpoint) {
+ // Validate endpoint format
+ try {
+ new URL(modelSettings.endpoint);
+ } catch {
+ return 'Invalid endpoint URL format';
+ }
+ }
+ }
+
+ return null;
+ };
+
+ const validateSearchSettings = (
+ searchSettings: SearchSettings,
+ ): string | null => {
+ if (!searchSettings.provider) {
+ return 'Search provider is required';
+ }
+ if (!searchSettings.apiKey) {
+ return 'API Key is required';
+ }
+
+ return null;
+ };
+
+ const saveSettings = async () => {
+ // Validate model settings
+ const modelError = validateModelSettings(settings.model);
+ if (modelError) {
+ toast.error(modelError);
+ return false;
+ }
+
+ // Validate search settings
+ const searchError = validateSearchSettings(settings.search);
+ if (searchError) {
+ toast.error(searchError);
+ return false;
+ }
+
+ try {
+ // Save all settings
+ saveLLMSettings(settings.model);
+ await updateLLMConfig(settings.model);
+
+ saveFileSystemSettings(settings.fileSystem);
+ await ipcClient.updateFileSystemConfig(settings.fileSystem);
+
+ saveSearchSettings(settings.search);
+ await ipcClient.updateSearchConfig(settings.search);
+
+ toast.success('Settings saved successfully');
+ return true;
+ } catch (error) {
+ toast.error('Failed to save settings: ' + (error as Error).message);
+ return false;
+ }
+ };
+
+ return {
+ settings,
+ setSettings,
+ saveSettings,
+ };
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/useModelSettings.ts b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/useModelSettings.ts
new file mode 100644
index 000000000..5a0129058
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/useModelSettings.ts
@@ -0,0 +1,42 @@
+import { useState, useEffect } from 'react';
+import {
+ loadLLMSettings,
+ saveLLMSettings,
+} from '../../../services/llmSettings';
+import { updateLLMConfig } from '../../../api/llmConfig';
+import { ModelProvider, ModelSettings } from '@agent-infra/shared';
+
+export function useModelSettings() {
+ const [settings, setSettings] = useState({
+ provider: ModelProvider.OPENAI,
+ model: 'gpt-4o',
+ apiKey: '',
+ apiVersion: '',
+ endpoint: '',
+ });
+
+ // Load settings from localStorage on mount
+ useEffect(() => {
+ const savedSettings = loadLLMSettings();
+ if (savedSettings) {
+ setSettings(savedSettings);
+ }
+ }, []);
+
+ const saveSettings = async () => {
+ // Save settings directly without modifying the model field
+ const finalSettings = { ...settings };
+
+ // Save to localStorage
+ saveLLMSettings(finalSettings);
+
+ // Update LLM configuration in main process
+ await updateLLMConfig(finalSettings);
+ };
+
+ return {
+ settings,
+ setSettings,
+ saveSettings,
+ };
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/useProviders.ts b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/useProviders.ts
new file mode 100644
index 000000000..c5533bbc3
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/Settings/useProviders.ts
@@ -0,0 +1,26 @@
+import { useState, useEffect } from 'react';
+import { getAvailableProviders } from '../../../api/llmConfig';
+import { ModelProvider } from '@agent-infra/shared';
+
+export function useProviders() {
+ const [providers, setProviders] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ async function fetchProviders() {
+ try {
+ const availableProviders =
+ (await getAvailableProviders()) as ModelProvider[];
+ setProviders(availableProviders);
+ } catch (error) {
+ console.error('Failed to fetch providers:', error);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ fetchProviders();
+ }, []);
+
+ return { providers, loading };
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/TopBar.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/TopBar.tsx
new file mode 100644
index 000000000..8145c740e
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/TopBar.tsx
@@ -0,0 +1,53 @@
+import { BiSidebar, BiEdit, BiMoon, BiSun } from 'react-icons/bi';
+import { Tooltip } from '@nextui-org/react';
+import styles from './index.module.scss';
+
+interface TopBarProps {
+ isCollapsed: boolean;
+ isDarkMode: boolean;
+ onToggleCollapse: () => void;
+ onToggleTheme: () => void;
+ onAddSession: () => void;
+}
+
+export function TopBar({
+ isCollapsed,
+ isDarkMode,
+ onToggleCollapse,
+ onToggleTheme,
+ onAddSession,
+}: TopBarProps) {
+ return (
+
+ {!isCollapsed &&
Agent TARS }
+
+
+
+
+
+
+
+
+ {isDarkMode ? : }
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/index.module.scss b/apps/agent-tars/src/renderer/src/components/LeftSidebar/index.module.scss
new file mode 100644
index 000000000..fec0d698c
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/index.module.scss
@@ -0,0 +1,395 @@
+.sidebar {
+ position: fixed;
+ top: 16px;
+ left: 0;
+ height: 100vh;
+ background: var(--ai-color-bg-secondary);
+ box-shadow: var(--shadow-md);
+ transition:
+ width var(--transition-smooth),
+ box-shadow var(--transition-smooth);
+ overflow: hidden;
+ border-right: 1px solid var(--ai-color-border);
+ display: flex;
+ flex-direction: column;
+ z-index: 10;
+
+ &.expanded {
+ width: 260px;
+ }
+
+ &.collapsed {
+ width: 56px;
+ }
+}
+
+.topbar {
+ flex-shrink: 0;
+ padding: 8px 12px 0;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+
+ &.collapsed {
+ margin-top: 0;
+ padding: 20px 0 0;
+ justify-content: center;
+ flex-direction: column;
+ height: auto;
+ gap: 12px;
+ }
+}
+
+.title {
+ margin: 0;
+ margin-left: 10px;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--ai-color-text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ animation: fadeIn 0.3s ease;
+}
+
+.toggleButton {
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--ai-color-border);
+ border-radius: var(--ai-radius-md);
+ background: var(--ai-color-bg-primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: transform var(--transition-normal);
+ color: var(--ai-color-text-tertiary);
+ z-index: 10;
+
+ &:hover {
+ background: var(--ai-color-bg-hover);
+ color: var(--ai-color-text-primary);
+ border-color: var(--ai-color-border-hover);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+}
+
+.messageList {
+ flex: 1;
+ padding: 8px;
+ padding-top: 0;
+ overflow-y: auto;
+
+ &::-webkit-scrollbar {
+ width: 4px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--ai-color-bg-quaternary);
+ border-radius: var(--radius-sm);
+
+ &:hover {
+ background: var(--ai-color-text-quaternary);
+ }
+ }
+
+ &.collapsed {
+ display: none;
+ }
+}
+
+.messageItem {
+ padding: 6px 12px;
+ margin: 0 4px 4px;
+ flex-shrink: 0;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: transform var(--transition-normal);
+ font-size: 14px;
+ color: var(--ai-color-text-primary);
+ background: var(--ai-color-bg-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ height: 36px;
+ padding: 0 12px;
+ box-sizing: border-box;
+
+ &:hover {
+ background: var(--ai-color-bg-hover);
+ color: var(--ai-color-text-primary);
+ }
+
+ &:active {
+ background: var(--ai-color-bg-active);
+ }
+
+ &.active {
+ background: var(--ai-color-primary-bg);
+ color: var(--ai-color-primary);
+ height: 36px;
+ font-weight: 500;
+ }
+
+ .moreButton {
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ }
+
+ &:hover {
+ .moreButton {
+ opacity: 0.7;
+
+ &:hover {
+ opacity: 1;
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+ }
+ }
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px;
+}
+
+.controls {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+
+ &.controlsCollapsed {
+ flex-direction: column;
+ }
+}
+
+.messageTitle {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.messageItem {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.deleteButton {
+ opacity: 0;
+ background: none;
+ border: none;
+ padding: 6px;
+ color: var(--ai-color-danger);
+ cursor: pointer;
+ border-radius: var(--radius-sm);
+ transition: all var(--transition-normal);
+
+ &:hover {
+ background: var(--ai-color-danger-bg);
+ color: var(--ai-color-danger-hover);
+ }
+}
+
+.messageItem:hover .deleteButton {
+ opacity: 1;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateX(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.editContainer {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ width: 100%;
+}
+
+.editInput {
+ flex: 1;
+ background: var(--ai-color-bg-primary);
+ border: 1px solid var(--ai-color-border);
+ border-radius: var(--radius-sm);
+ padding: 2px 8px;
+ font-size: 14px;
+ font-weight: normal;
+ color: var(--ai-color-text-primary);
+ height: 24px;
+
+ &:focus {
+ outline: none;
+ border-color: var(--ai-color-primary);
+ }
+}
+
+.actionButtons {
+ display: flex;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity var(--transition-normal);
+ height: 100%;
+ display: flex;
+ align-items: center;
+}
+
+.messageItem:hover .actionButtons {
+ opacity: 1;
+}
+
+.editButton {
+ opacity: 0;
+ border: none;
+ padding: 6px;
+ color: var(--ai-color-text-tertiary);
+ cursor: pointer;
+ border-radius: var(--radius-sm);
+ transition: all var(--transition-normal);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+
+ &:hover {
+ color: var(--ai-color-text-primary);
+ }
+
+ &:active {
+ background: var(--ai-color-button-active);
+ }
+}
+
+.messageItem:hover .editButton {
+ opacity: 1;
+}
+
+.editContainer .editButton {
+ color: #22c55e;
+
+ &:hover {
+ color: #16a34a;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateX(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.itemContent {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding-right: 4px;
+}
+
+.moreButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ padding: 4px;
+ color: inherit;
+ opacity: 0.7;
+ cursor: pointer;
+ border-radius: 4px;
+
+ &:hover {
+ opacity: 1;
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+}
+
+.aiNav {
+ width: 100%;
+ padding: 8px;
+}
+
+.navItem {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 6px 12px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: var(--ai-color-text-secondary);
+ margin-bottom: 2px;
+
+ &:hover {
+ background: var(--ai-color-bg-hover);
+ color: var(--ai-color-text-primary);
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+}
+
+.navIcon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+}
+
+.navTitle {
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.sidebarPlaceholder {
+ flex-shrink: 0;
+ transition: width var(--transition-smooth);
+
+ &.expanded {
+ width: 260px;
+ }
+
+ &.collapsed {
+ width: 56px;
+ }
+}
+
+.bottomBar {
+ padding: 12px;
+ display: flex;
+ justify-content: center;
+ margin-bottom: 12px;
+
+ &.collapsed {
+ padding: 0px 0;
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/index.tsx b/apps/agent-tars/src/renderer/src/components/LeftSidebar/index.tsx
new file mode 100644
index 000000000..2b66ece40
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/index.tsx
@@ -0,0 +1,157 @@
+import styles from './index.module.scss';
+import { atom, useAtom } from 'jotai';
+import { useEffect, useState } from 'react';
+import { useThemeMode } from '@renderer/hooks/useThemeMode';
+import { TopBar } from './TopBar';
+import { BottomBar } from './BottomBar';
+import { DeleteModal } from './DeleteModal';
+import { SettingsModal } from './Settings';
+import { SessionList } from './SessionList';
+import { useChatSessions } from '@renderer/hooks/useChatSession';
+import { useAppChat } from '@renderer/hooks/useAppChat';
+import toast from 'react-hot-toast';
+
+export const leftSidebarCollapsedAtom = atom(false);
+export const DEFAULT_APP_ID = 'omega-agent';
+
+export function LeftSidebar() {
+ const [isCollapsed, setIsCollapsed] = useAtom(leftSidebarCollapsedAtom);
+ const { toggle: toggleTheme, value: isDarkMode } = useThemeMode();
+ const [deleteModalOpen, setDeleteModalOpen] = useState(false);
+ const [settingsModalOpen, setSettingsModalOpen] = useState(false);
+ const [sessionToDelete, setSessionToDelete] = useState(null);
+ const [editingSessionId, setEditingSessionId] = useState(null);
+ const [editingName, setEditingName] = useState('');
+ const { messageSending } = useAppChat();
+ const {
+ chatSessions,
+ currentSessionId,
+ updateCurrentSessionId,
+ addNewSession,
+ removeSession,
+ initializeSessions,
+ updateChatSession,
+ } = useChatSessions({
+ appId: 'omega-agent',
+ origin: 'omega',
+ });
+
+ useEffect(() => {
+ initializeSessions();
+ }, [initializeSessions]);
+
+ const handleAddSession = () => {
+ addNewSession({
+ appId: 'omega-agent',
+ name: 'New Session',
+ messageCount: 0,
+ origin: 'ami',
+ });
+ };
+
+ const handleDeleteSession = (sessionId: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ setSessionToDelete(sessionId);
+ setDeleteModalOpen(true);
+ };
+
+ const confirmDelete = () => {
+ if (sessionToDelete) {
+ removeSession(sessionToDelete);
+ setDeleteModalOpen(false);
+ setSessionToDelete(null);
+ }
+ };
+
+ const handleEditSession = (
+ sessionId: string,
+ currentName: string,
+ e: React.MouseEvent,
+ ) => {
+ e.stopPropagation();
+ setEditingSessionId(sessionId);
+ setEditingName(currentName);
+ };
+
+ const handleSaveEdit = async (sessionId: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (editingName.trim()) {
+ await updateChatSession(sessionId, { name: editingName.trim() });
+ setEditingSessionId(null);
+ }
+ };
+
+ const handleKeyDown = async (e: React.KeyboardEvent, sessionId: string) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ if (editingName.trim()) {
+ await updateChatSession(sessionId, { name: editingName.trim() });
+ setEditingSessionId(null);
+ }
+ } else if (e.key === 'Escape') {
+ setEditingSessionId(null);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
setIsCollapsed(!isCollapsed)}
+ onToggleTheme={toggleTheme}
+ onAddSession={handleAddSession}
+ />
+
+
+ {
+ if (messageSending) {
+ toast.error('Please finish sending message first');
+ return;
+ }
+ updateCurrentSessionId(sessionId);
+ }}
+ />
+
+
+ setSettingsModalOpen(true)}
+ />
+
+
+ {
+ setDeleteModalOpen(false);
+ setSessionToDelete(null);
+ }}
+ onConfirm={confirmDelete}
+ />
+
+ setSettingsModalOpen(false)}
+ />
+ >
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LeftSidebar/type.ts b/apps/agent-tars/src/renderer/src/components/LeftSidebar/type.ts
new file mode 100644
index 000000000..aaeb1b836
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LeftSidebar/type.ts
@@ -0,0 +1,10 @@
+export interface ChatSession {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ id: string;
+ name: string;
+ appId: string;
+ messageCount: number;
+ origin?: string;
+ createdAt?: string;
+ updatedAt?: string;
+}
diff --git a/apps/agent-tars/src/renderer/src/components/LoadingSpinner/index.tsx b/apps/agent-tars/src/renderer/src/components/LoadingSpinner/index.tsx
new file mode 100644
index 000000000..4530829ae
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/LoadingSpinner/index.tsx
@@ -0,0 +1,37 @@
+import { Loader2 } from 'lucide-react';
+import styled, { keyframes } from 'styled-components';
+
+const spin = keyframes`
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+`;
+
+const SpinnerWrapper = styled.div<{ size?: number; color?: string }>`
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+
+ svg {
+ animation: ${spin} 1.5s linear infinite;
+ width: ${(props) => props.size || 16}px;
+ height: ${(props) => props.size || 16}px;
+ color: ${(props) => props.color || 'var(--ai-color-primary)'};
+ }
+`;
+
+interface LoadingSpinnerProps {
+ size?: number;
+ color?: string;
+}
+
+export function LoadingSpinner({ size, color }: LoadingSpinnerProps) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/components/MonacoEditor.tsx b/apps/agent-tars/src/renderer/src/components/MonacoEditor.tsx
new file mode 100644
index 000000000..3303b7032
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/components/MonacoEditor.tsx
@@ -0,0 +1,106 @@
+import { useThemeMode } from '@renderer/hooks/useThemeMode';
+import { Editor, DiffEditor, loader } from '@monaco-editor/react';
+import { defaultEditorOptions } from '@renderer/utils/monacoConfig';
+import * as monaco from 'monaco-editor';
+import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
+import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
+import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
+import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
+import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
+import { isReportHtmlMode } from '@renderer/constants';
+
+loader.config({
+ paths: {
+ vs: isReportHtmlMode
+ ? 'https://unpkg.com/monaco-editor@0.44.0/min/vs'
+ : 'monaco-editor/esm/vs',
+ },
+ 'vs/nls': {
+ availableLanguages: {},
+ },
+ monaco,
+});
+
+if (window.MonacoEnvironment === undefined && !isReportHtmlMode) {
+ window.MonacoEnvironment = {
+ getWorker(_, label) {
+ if (label === 'json') {
+ return new jsonWorker();
+ }
+ if (label === 'css' || label === 'scss' || label === 'less') {
+ return new cssWorker();
+ }
+ if (label === 'html' || label === 'handlebars' || label === 'razor') {
+ return new htmlWorker();
+ }
+ if (label === 'typescript' || label === 'javascript') {
+ return new tsWorker();
+ }
+ return new editorWorker();
+ },
+ };
+}
+
+export function MonacoEditor({
+ language,
+ value,
+ readOnly = true,
+ height = '100%',
+ original,
+ isDiff = false,
+}: MonacoEditorProps) {
+ const isDarkMode = useThemeMode();
+ const theme = isDarkMode.value ? 'vs-dark' : 'vs';
+
+ if (isDiff && original !== undefined) {
+ return (
+
+ Loading editor...
+
+ }
+ />
+ );
+ }
+
+ return (
+
+ Loading editor...
+
+ }
+ beforeMount={(monaco) => {
+ monaco.editor.onDidCreateModel(() => {});
+ }}
+ />
+ );
+}
+
+interface MonacoEditorProps {
+ language: string;
+ value: string;
+ readOnly?: boolean;
+ height?: string | number;
+ original?: string;
+ isDiff?: boolean;
+}
diff --git a/apps/agent-tars/src/renderer/src/constants/index.ts b/apps/agent-tars/src/renderer/src/constants/index.ts
new file mode 100644
index 000000000..42335583e
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/constants/index.ts
@@ -0,0 +1,15 @@
+import { ToolCallType } from '@renderer/type/agent';
+
+export const STORAGE_DB_NAME = 'open-agent-chat';
+
+export const SNAPSHOT_BROWSER_ACTIONS = [
+ ToolCallType.BrowserHover,
+ ToolCallType.BrowserNewTab,
+ ToolCallType.BrowserNavigate,
+ ToolCallType.BrowserSelect,
+ ToolCallType.BrowserClick,
+ ToolCallType.BrowserFormInputFill,
+ ToolCallType.BrowserSwitchTab,
+ ToolCallType.BrowserScroll,
+];
+export const isReportHtmlMode = Boolean(process.env.REPORT_HTML_MODE);
diff --git a/apps/agent-tars/src/renderer/src/env.d.ts b/apps/agent-tars/src/renderer/src/env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/apps/agent-tars/src/renderer/src/hooks/useAddUserMessage.ts b/apps/agent-tars/src/renderer/src/hooks/useAddUserMessage.ts
new file mode 100644
index 000000000..cfe79dc2e
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/hooks/useAddUserMessage.ts
@@ -0,0 +1,56 @@
+import {
+ InputFile,
+ InputFileType,
+ MessageRole,
+ MessageType,
+} from '@vendor/chat-ui';
+import { useAppChat } from './useAppChat';
+import { useCallback } from 'react';
+
+export function useAddUserMessage() {
+ const { addMessage } = useAppChat();
+
+ const addUserMessage = useCallback(
+ async (inputText: string, inputFiles: InputFile[]) => {
+ await addMessage(
+ {
+ type: MessageType.PlainText,
+ content: inputText,
+ role: MessageRole.User,
+ timestamp: Date.now(),
+ },
+ {
+ shouldSyncStorage: true,
+ },
+ );
+
+ if (inputFiles.length > 0) {
+ for (const file of inputFiles) {
+ const normalizedFile =
+ file.type === InputFileType.Image
+ ? file
+ : {
+ ...file,
+ content: '',
+ };
+
+ await addMessage(
+ {
+ role: MessageRole.User,
+ type: MessageType.File,
+ content: normalizedFile,
+ isFinal: true,
+ timestamp: Date.now(),
+ },
+ {
+ shouldSyncStorage: true,
+ },
+ );
+ }
+ }
+ },
+ [addMessage],
+ );
+
+ return addUserMessage;
+}
diff --git a/apps/agent-tars/src/renderer/src/hooks/useAgentFlow.ts b/apps/agent-tars/src/renderer/src/hooks/useAgentFlow.ts
new file mode 100644
index 000000000..948ec25b6
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/hooks/useAgentFlow.ts
@@ -0,0 +1,97 @@
+import { useCallback } from 'react';
+import { useAppChat } from './useAppChat';
+import { InputFile, MessageRole } from '@vendor/chat-ui';
+import { AgentFlow } from '../agent/AgentFlow';
+import { EventItem } from '@renderer/type/event';
+import { useAtom } from 'jotai';
+import {
+ agentStatusTipAtom,
+ currentAgentFlowIdRefAtom,
+ currentEventIdAtom,
+ eventsAtom,
+ planTasksAtom,
+} from '@renderer/state/chat';
+import { v4 as uuid } from 'uuid';
+import { PlanTask } from '@renderer/type/agent';
+import { showCanvasAtom } from '@renderer/state/canvas';
+import { useChatSessions } from './useChatSession';
+import { DEFAULT_APP_ID } from '@renderer/components/LeftSidebar';
+import { ipcClient } from '@renderer/api';
+import { Message } from '@agent-infra/shared';
+
+export interface AppContext {
+ chatUtils: ReturnType;
+ request: {
+ inputText: string;
+ inputFiles: InputFile[];
+ };
+ agentFlowId: string;
+ setEventId: (eventId: string) => void;
+ setEvents: React.Dispatch>;
+ setAgentStatusTip: (status: string) => void;
+ setPlanTasks: (tasks: PlanTask[]) => void;
+ setShowCanvas: (show: boolean) => void;
+}
+
+export function useAgentFlow() {
+ const chatUtils = useAppChat();
+ const [, setEvents] = useAtom(eventsAtom);
+ const [, setAgentStatusTip] = useAtom(agentStatusTipAtom);
+ const [currentAgentFlowIdRef] = useAtom(currentAgentFlowIdRefAtom);
+ const [, setShowCanvas] = useAtom(showCanvasAtom);
+ const [, setEventId] = useAtom(currentEventIdAtom);
+ const [, setPlanTasks] = useAtom(planTasksAtom);
+ const { updateChatSession, currentSessionId } = useChatSessions({
+ appId: DEFAULT_APP_ID,
+ });
+
+ const updateSessionTitle = useCallback(
+ async (input: string) => {
+ if (!currentSessionId) {
+ return;
+ }
+ const userMessages = chatUtils.messages
+ .filter((m) => m.role === MessageRole.User)
+ .slice(-5);
+ const userMessageContent =
+ userMessages.map((m) => m.content).join('\n') + input;
+ const result = await ipcClient.askLLMText({
+ messages: [
+ Message.systemMessage(
+ `You are conversation summary expert.Please give a title for the coversation topic, the topic should be no more than 20 words.You can only output the topic content, don't output any other words.Use the same with as the language of the user input.The language should be the same as the user input.`,
+ ),
+ Message.userMessage(
+ `user input: ${userMessageContent}, please give me the topic title.`,
+ ),
+ ],
+ requestId: uuid(),
+ });
+ await updateChatSession(currentSessionId, {
+ name: result,
+ });
+ },
+ [currentSessionId, updateChatSession, chatUtils.messages],
+ );
+
+ return useCallback(
+ async (inputText: string, inputFiles: InputFile[]) => {
+ const agentFlowId = uuid();
+ currentAgentFlowIdRef.current = agentFlowId;
+ const agentFlow = new AgentFlow({
+ chatUtils,
+ setEvents,
+ setEventId,
+ setAgentStatusTip,
+ setPlanTasks,
+ setShowCanvas,
+ agentFlowId,
+ request: {
+ inputText,
+ inputFiles,
+ },
+ });
+ await Promise.all([agentFlow.run(), updateSessionTitle(inputText)]);
+ },
+ [chatUtils, setEvents, updateSessionTitle],
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/hooks/useAppChat.ts b/apps/agent-tars/src/renderer/src/hooks/useAppChat.ts
new file mode 100644
index 000000000..2c947b727
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/hooks/useAppChat.ts
@@ -0,0 +1,15 @@
+import { useChat } from '@vendor/chat-ui';
+import { STORAGE_DB_NAME } from '@renderer/constants';
+import { MessageContentType } from '@renderer/type/chatMessage';
+import { useChatSessions } from './useChatSession';
+import { DEFAULT_APP_ID } from '@renderer/components/LeftSidebar';
+
+export function useAppChat() {
+ const { currentSessionId } = useChatSessions({
+ appId: DEFAULT_APP_ID,
+ });
+ return useChat({
+ storageDbName: STORAGE_DB_NAME,
+ conversationId: currentSessionId || 'default',
+ });
+}
diff --git a/apps/agent-tars/src/renderer/src/hooks/useChatSession.ts b/apps/agent-tars/src/renderer/src/hooks/useChatSession.ts
new file mode 100644
index 000000000..ca42aed07
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/hooks/useChatSession.ts
@@ -0,0 +1,171 @@
+import { ChatSession } from '@renderer/components/LeftSidebar/type';
+import { useAtom, atom } from 'jotai';
+import { useCallback } from 'react';
+import {
+ createSession,
+ deleteSession,
+ getSessions,
+ updateSession,
+} from '@renderer/services/chatSessionStorage';
+
+// Create atoms for each app's chat sessions and current session ID
+const createAppAtoms = () => {
+ const chatSessionsAtom = atom([]);
+ const currentSessionIdAtom = atom(null);
+ const initStateRefAtom = atom<{
+ current: 'pending' | 'loading' | 'finished';
+ }>({ current: 'pending' });
+ return { chatSessionsAtom, currentSessionIdAtom, initStateRefAtom };
+};
+
+// Create a map to store atoms for each app
+const appAtomsMap = new Map>();
+
+// eslint-disable-next-line max-lines-per-function
+export function useChatSessions({
+ appId,
+ origin,
+ onSwitchSession,
+}: {
+ appId: string;
+ origin?: string;
+ onSwitchSession?: (session: ChatSession) => void | Promise;
+}) {
+ // Get or create atoms for the current app
+ if (!appAtomsMap.has(appId)) {
+ appAtomsMap.set(appId, createAppAtoms());
+ }
+ const { chatSessionsAtom, currentSessionIdAtom, initStateRefAtom } =
+ appAtomsMap.get(appId)!;
+
+ const [chatSessions, setChatSessions] = useAtom(chatSessionsAtom);
+ const [currentSessionId, setCurrentSessionId] = useAtom(currentSessionIdAtom);
+ const [initStateRef] = useAtom(initStateRefAtom);
+
+ const updateChatSession = useCallback(
+ async (
+ sessionId: string,
+ newSessionData: Partial,
+ options: {
+ shouldSyncRemote: boolean;
+ } = { shouldSyncRemote: true },
+ ) => {
+ if (options.shouldSyncRemote) {
+ await updateSession(sessionId, newSessionData);
+ }
+ setChatSessions((sessions) =>
+ sessions.map((session) =>
+ session.id === sessionId
+ ? {
+ ...session,
+ ...newSessionData,
+ }
+ : session,
+ ),
+ );
+ },
+ [setChatSessions],
+ );
+
+ const updateCurrentSessionId = useCallback(
+ async (newSessionId: string, callback?: () => void) => {
+ setCurrentSessionId(newSessionId);
+ localStorage.setItem(`${appId}-current-chat-session-id`, newSessionId);
+ const targetSession = chatSessions.find(
+ (session) => session.id === newSessionId,
+ );
+ await onSwitchSession?.(targetSession!);
+ callback?.();
+ },
+ [appId, setCurrentSessionId, chatSessions],
+ );
+
+ const addNewSession = useCallback(
+ async (sessionData: Omit) => {
+ const newSession = await createSession(sessionData);
+ setChatSessions((sessions) => [...sessions, newSession]);
+ updateCurrentSessionId(newSession.id!);
+ },
+ [setChatSessions, updateCurrentSessionId],
+ );
+
+ const removeSession = useCallback(
+ async (sessionId: string) => {
+ await deleteSession(sessionId);
+ setChatSessions((sessions) => {
+ const updatedSessions = sessions.filter(
+ (session) => session.id !== sessionId,
+ );
+ if (sessionId === currentSessionId && updatedSessions.length > 0) {
+ updateCurrentSessionId(
+ updatedSessions[updatedSessions.length - 1].id!,
+ );
+ } else if (updatedSessions.length === 0) {
+ setCurrentSessionId(null);
+ localStorage.removeItem(`${appId}-current-chat-session-id`);
+ }
+ return updatedSessions;
+ });
+ },
+ [
+ appId,
+ currentSessionId,
+ setChatSessions,
+ setCurrentSessionId,
+ updateCurrentSessionId,
+ ],
+ );
+
+ const initializeSessions = useCallback(async () => {
+ if (initStateRef.current === 'pending') {
+ console.log('初始化 session...');
+ initStateRef.current = 'loading';
+ const storedCurrentSessionId = localStorage.getItem(
+ `${appId}-current-chat-session-id`,
+ );
+
+ // Get all sessions from API
+ const sessions = await getSessions(appId);
+
+ if (sessions.length > 0) {
+ setChatSessions(sessions);
+
+ if (
+ storedCurrentSessionId &&
+ sessions.some((s) => s.id === storedCurrentSessionId)
+ ) {
+ // Restore previous session if it exists
+ setCurrentSessionId(storedCurrentSessionId);
+ await onSwitchSession?.(
+ sessions.find((s) => s.id === storedCurrentSessionId)!,
+ );
+ } else {
+ // Use the last session if stored session not found
+ updateCurrentSessionId(sessions[sessions.length - 1].id!);
+ }
+ } else {
+ // Create default session if no sessions exist
+ const defaultSession = await createSession({
+ appId,
+ name: '当前会话',
+ messageCount: 0,
+ origin,
+ });
+ setChatSessions([defaultSession]);
+ updateCurrentSessionId(defaultSession.id!);
+ }
+ }
+ initStateRef.current = 'finished';
+ }, []);
+
+ return {
+ currentSessionId,
+ updateChatSession,
+ updateCurrentSessionId,
+ addNewSession,
+ chatSessions,
+ removeSession,
+ initializeSessions,
+ initStateRef,
+ };
+}
diff --git a/apps/agent-tars/src/renderer/src/hooks/useFileSystemSettings.ts b/apps/agent-tars/src/renderer/src/hooks/useFileSystemSettings.ts
new file mode 100644
index 000000000..84c884708
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/hooks/useFileSystemSettings.ts
@@ -0,0 +1,57 @@
+import { useEffect, useState } from 'react';
+import { ipcClient } from '@renderer/api';
+import {
+ loadFileSystemSettings,
+ saveFileSystemSettings,
+} from '@renderer/services/fileSystemSettings';
+
+export function useFileSystemSettings() {
+ const [initialized, setInitialized] = useState(false);
+
+ useEffect(() => {
+ async function initFileSystemSettings() {
+ try {
+ // Get current allowed directories from main process
+ const allowedDirectories = await ipcClient.getAllowedDirectories();
+
+ // Load settings from localStorage
+ const settings = loadFileSystemSettings();
+
+ // If no settings exist, create them with the allowed directories
+ if (!settings) {
+ saveFileSystemSettings({ availableDirectories: allowedDirectories });
+ } else {
+ // Make sure the settings match the main process
+ const updatedSettings = {
+ ...settings,
+ availableDirectories: Array.from(
+ new Set([
+ ...settings.availableDirectories,
+ ...allowedDirectories,
+ ]),
+ ),
+ };
+
+ // Save updated settings
+ saveFileSystemSettings(updatedSettings);
+
+ // Update main process if needed
+ if (
+ JSON.stringify(updatedSettings.availableDirectories) !==
+ JSON.stringify(allowedDirectories)
+ ) {
+ await ipcClient.updateFileSystemConfig(updatedSettings);
+ }
+ }
+
+ setInitialized(true);
+ } catch (error) {
+ console.error('Failed to initialize file system settings:', error);
+ }
+ }
+
+ initFileSystemSettings();
+ }, []);
+
+ return { initialized };
+}
diff --git a/apps/agent-tars/src/renderer/src/hooks/useThemeMode.ts b/apps/agent-tars/src/renderer/src/hooks/useThemeMode.ts
new file mode 100644
index 000000000..bbbb29b20
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/hooks/useThemeMode.ts
@@ -0,0 +1,11 @@
+import useDarkMode from 'use-dark-mode';
+
+window.global = window;
+
+export const useThemeMode = () =>
+ useDarkMode(false, {
+ classNameDark: 'dark',
+ classNameLight: 'light',
+ element: window.document.documentElement,
+ storageKey: 'open-agent-dark-mode',
+ });
diff --git a/apps/agent-tars/src/renderer/src/index.scss b/apps/agent-tars/src/renderer/src/index.scss
new file mode 100644
index 000000000..19228abf3
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/index.scss
@@ -0,0 +1,195 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ /* Colors */
+ --ai-color-primary: #3b82f6; /* blue-500 */
+ --ai-color-primary-hover: #60a5fa; /* blue-400 */
+ --ai-color-primary-active: #2563eb; /* blue-600 */
+ --ai-color-primary-bg: #dbeafe; /* blue-100 */
+
+ /* Text Colors */
+ --ai-color-text-primary: #1a1a1a;
+ --ai-color-text-secondary: #454545;
+ --ai-color-text-tertiary: #666666;
+ --ai-color-text-quaternary: #999999;
+
+ /* Background Colors */
+ --ai-color-bg-primary: #ffffff;
+ --ai-color-bg-secondary: #fafafa;
+ --ai-color-bg-tertiary: #f5f6f7;
+ --ai-color-bg-quaternary: #f0f0f0;
+ --ai-color-bg-hover: #f0f0f0;
+ --ai-color-bg-active: #ebedf0;
+
+ /* Border Colors */
+ --ai-color-border: #f0f0f0;
+ --ai-color-border-hover: #e0e0e0;
+
+ /* Shadow */
+ --ai-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ --ai-shadow-md: 0 0 20px rgba(0, 0, 0, 0.05);
+
+ /* Border Radius */
+ --ai-radius-sm: 4px;
+ --ai-radius-md: 8px;
+ --radius-lg: 20px;
+
+ /* Transitions */
+ --transition-normal: 0.2s ease;
+ --transition-smooth: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+
+ --ai-color-scrollbar: rgba(0, 0, 0, 0.2);
+ --ai-color-scrollbar-hover: rgba(0, 0, 0, 0.3);
+ --ai-color-scrollbar-bg: rgba(0, 0, 0, 0.05);
+
+ /* 滚动条相关变量 */
+ --ai-scrollbar-width: 12px;
+ --ai-scrollbar-height: 8px;
+ --ai-scrollbar-thumb-color: rgba(60, 60, 60, 0.18);
+ --ai-scrollbar-track-color: transparent;
+ --ai-scrollbar-border-radius: 6px;
+
+ /* Danger Colors */
+ --ai-color-danger: #dc2626;
+ --ai-color-danger-hover: #ef4444;
+ --ai-color-danger-bg: #fee2e2;
+
+ /* Button Colors */
+ --ai-color-button-bg: var(--ai-color-bg-tertiary);
+ --ai-color-button-hover: var(--ai-color-bg-quaternary);
+ --ai-color-button-active: var(--ai-color-bg-active);
+ --ai-color-button-text: var(--ai-color-text-tertiary);
+ --ai-color-button-text-hover: var(--ai-color-text-primary);
+}
+
+.dark {
+ /* Colors */
+ --ai-color-primary: #3b82f6; /* blue-500 */
+ --ai-color-primary-hover: #60a5fa; /* blue-400 */
+ --ai-color-primary-active: #2563eb; /* blue-600 */
+ --ai-color-primary-bg: #1e3a8a; /* blue-900,暗色主题也相应调整深度 */
+
+ /* Text Colors */
+ --ai-color-text-primary: #e6e6e6;
+ --ai-color-text-secondary: #cccccc;
+ --ai-color-text-tertiary: #999999;
+ --ai-color-text-quaternary: #666666;
+
+ /* Background Colors */
+ --ai-color-bg-primary: #1a1a1a;
+ --ai-color-bg-secondary: #242424;
+ --ai-color-bg-tertiary: #2a2a2a;
+ --ai-color-bg-quaternary: #333333;
+ --ai-color-bg-hover: #2c2c2c;
+ --ai-color-bg-active: #363636;
+
+ /* Border Colors */
+ --ai-color-border: #303030;
+ --ai-color-border-hover: #404040;
+
+ /* Shadow */
+ --ai-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
+ --ai-shadow-md: 0 0 20px rgba(0, 0, 0, 0.2);
+
+ --ai-color-scrollbar: rgba(255, 255, 255, 0.2);
+ --ai-color-scrollbar-hover: rgba(255, 255, 255, 0.3);
+ --ai-color-scrollbar-bg: rgba(255, 255, 255, 0.05);
+
+ /* 滚动条相关变量 */
+ --ai-scrollbar-width: 12px;
+ --ai-scrollbar-height: 8px;
+ --ai-scrollbar-thumb-color: rgba(255, 255, 255, 0.18);
+ --ai-scrollbar-track-color: transparent;
+
+ /* Danger Colors */
+ --ai-color-danger: #ef4444;
+ --ai-color-danger-hover: #dc2626;
+ --ai-color-danger-bg: #451a1a;
+
+ /* Button Colors */
+ --ai-color-button-bg: var(--ai-color-bg-tertiary);
+ --ai-color-button-hover: var(--ai-color-bg-quaternary);
+ --ai-color-button-active: var(--ai-color-bg-active);
+ --ai-color-button-text: var(--ai-color-text-tertiary);
+ --ai-color-button-text-hover: var(--ai-color-text-primary);
+}
+
+body {
+ color: var(--ai-color-text-primary);
+ background: var(--ai-color-bg-primary);
+}
+
+.scrollbar {
+ scrollbar-gutter: stable;
+
+ /* 滚动条大小 */
+ &::-webkit-scrollbar {
+ width: var(--ai-scrollbar-width);
+ height: var(--ai-scrollbar-height);
+ transition: all 0.3s;
+ }
+
+ /* 滚动条轨道 */
+ &::-webkit-scrollbar-track {
+ background: var(--ai-scrollbar-track-color);
+ }
+
+ /* 滚动条滑块 */
+ &::-webkit-scrollbar-thumb {
+ border-radius: var(--ai-scrollbar-border-radius);
+ }
+
+ /* 鼠标悬停时的滑块样式 */
+ &:hover::-webkit-scrollbar-thumb {
+ border: 3px solid transparent;
+ background-color: var(--ai-scrollbar-thumb-color);
+ background-clip: content-box;
+ }
+
+ /* 横向滚动条滑块 */
+ &::-webkit-scrollbar-thumb:horizontal {
+ border: 3px solid transparent;
+ background-color: var(--ai-scrollbar-thumb-color);
+ background-clip: content-box;
+ }
+
+ /* 滚动条交叉处 */
+ &::-webkit-scrollbar-corner {
+ background: transparent;
+ }
+
+ /* Firefox 滚动条样式 */
+ scrollbar-width: thin;
+ scrollbar-color: var(--ai-scrollbar-thumb-color)
+ var(--ai-scrollbar-track-color);
+}
+
+@keyframes flash {
+ 0% {
+ transform: translateX(-100%);
+ opacity: 0;
+ }
+ 50% {
+ transform: translateX(100%);
+ opacity: 0.5;
+ }
+ 100% {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+}
+
+@keyframes spin {
+ from {
+ transform: perspective(100px) rotateX(10deg) rotateY(10deg) rotate(0deg);
+ }
+ to {
+ transform: perspective(100px) rotateX(10deg) rotateY(10deg) rotate(360deg);
+ }
+}
+
+.animate-spin-slow {
+ animation: spin 3s linear infinite;
+}
diff --git a/apps/agent-tars/src/renderer/src/main.tsx b/apps/agent-tars/src/renderer/src/main.tsx
new file mode 100644
index 000000000..ca9c5e42d
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/main.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.scss';
+import { initMonacoWorkers } from './utils/monacoConfig';
+
+initMonacoWorkers();
+
+ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
+
+
+ ,
+);
diff --git a/apps/agent-tars/src/renderer/src/services/chatSessionStorage.ts b/apps/agent-tars/src/renderer/src/services/chatSessionStorage.ts
new file mode 100644
index 000000000..1d67f3307
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/services/chatSessionStorage.ts
@@ -0,0 +1,116 @@
+import localforage from 'localforage';
+import { v4 as uuidv4 } from 'uuid';
+import { ChatSession } from '@renderer/components/LeftSidebar/type';
+
+const chatSessionsStore = localforage.createInstance({
+ name: 'chatSessions',
+ description: 'Storage for chat sessions',
+});
+
+export async function getSessions(appId: string): Promise {
+ try {
+ const allSessions =
+ (await chatSessionsStore.getItem>(
+ 'sessions',
+ )) || {};
+ return allSessions[appId] || [];
+ } catch (error) {
+ console.error('Error getting sessions:', error);
+ return [];
+ }
+}
+
+export async function createSession(
+ sessionData: Omit,
+): Promise {
+ try {
+ const newSession: ChatSession = {
+ ...sessionData,
+ id: uuidv4(),
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+ const allSessions =
+ (await chatSessionsStore.getItem>(
+ 'sessions',
+ )) || {};
+ const appSessions = allSessions[sessionData.appId] || [];
+
+ allSessions[sessionData.appId] = [...appSessions, newSession];
+
+ await chatSessionsStore.setItem('sessions', allSessions);
+
+ return newSession;
+ } catch (error) {
+ console.error('Error creating session:', error);
+ throw new Error('Failed to create chat session');
+ }
+}
+
+export async function updateSession(
+ sessionId: string,
+ newSessionData: Partial,
+): Promise {
+ try {
+ const allSessions =
+ (await chatSessionsStore.getItem>(
+ 'sessions',
+ )) || {};
+
+ let sessionUpdated = false;
+
+ for (const appId in allSessions) {
+ allSessions[appId] = allSessions[appId].map((session) => {
+ if (session.id === sessionId) {
+ sessionUpdated = true;
+ return {
+ ...session,
+ ...newSessionData,
+ updatedAt: new Date().toISOString(),
+ };
+ }
+ return session;
+ });
+ }
+
+ if (!sessionUpdated) {
+ throw new Error(`Session with id ${sessionId} not found`);
+ }
+
+ await chatSessionsStore.setItem('sessions', allSessions);
+ } catch (error) {
+ console.error('Error updating session:', error);
+ throw new Error('Failed to update chat session');
+ }
+}
+
+export async function deleteSession(sessionId: string): Promise {
+ try {
+ const allSessions =
+ (await chatSessionsStore.getItem>(
+ 'sessions',
+ )) || {};
+
+ let sessionDeleted = false;
+
+ for (const appId in allSessions) {
+ const originalLength = allSessions[appId].length;
+ allSessions[appId] = allSessions[appId].filter(
+ (session) => session.id !== sessionId,
+ );
+
+ if (allSessions[appId].length < originalLength) {
+ sessionDeleted = true;
+ }
+ }
+
+ if (!sessionDeleted) {
+ throw new Error(`Session with id ${sessionId} not found`);
+ }
+
+ await chatSessionsStore.setItem('sessions', allSessions);
+ } catch (error) {
+ console.error('Error deleting session:', error);
+ throw new Error('Failed to delete chat session');
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/services/filePermissionService.ts b/apps/agent-tars/src/renderer/src/services/filePermissionService.ts
new file mode 100644
index 000000000..a429cc26b
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/services/filePermissionService.ts
@@ -0,0 +1,92 @@
+import { atom, getDefaultStore } from 'jotai';
+import { isPathAllowed, getDefaultDirectory } from './fileSystemSettings';
+import path from 'path-browserify';
+
+interface PermissionRequest {
+ path: string;
+ promise: Promise;
+ resolve: (value: boolean) => void;
+ reject: (reason: any) => void;
+}
+
+export const pendingPermissionRequestAtom = atom(
+ null,
+);
+
+/**
+ * Normalize a file path based on permissions
+ * @param filePath The file path to normalize
+ * @returns Normalized path (absolute)
+ */
+export function normalizePath(filePath: string): string {
+ // If it's already an absolute path, return it
+ if (filePath.startsWith('/')) {
+ return filePath;
+ }
+
+ // Otherwise, make it relative to the default directory
+ const defaultDir = getDefaultDirectory();
+ if (!defaultDir) {
+ throw new Error('No default directory configured');
+ }
+
+ return path.join(defaultDir, filePath);
+}
+
+/**
+ * Check if a file operation is allowed and request permission if needed
+ * @param filePath The file path to check
+ * @returns Promise that resolves to true if allowed, false if denied
+ */
+export async function checkPathPermission(filePath: string): Promise {
+ // Normalize path first
+ const normalizedPath = normalizePath(filePath);
+
+ // If path is allowed, return immediately
+ if (isPathAllowed(normalizedPath)) {
+ return true;
+ }
+
+ // Get current atom value using getDefaultStore
+ const store = getDefaultStore();
+ const currentRequest = store.get(pendingPermissionRequestAtom);
+
+ // If there's already a pending request for this path, return its promise
+ if (currentRequest && currentRequest.path === normalizedPath) {
+ return currentRequest.promise;
+ }
+
+ // Otherwise, create a new permission request
+ let resolvePromise: (value: boolean) => void = () => {};
+ let rejectPromise: (reason: any) => void = () => {};
+
+ const promise = new Promise((resolve, reject) => {
+ resolvePromise = resolve;
+ rejectPromise = reject;
+ });
+
+ // Update the atom with the new request using getDefaultStore
+ store.set(pendingPermissionRequestAtom, {
+ path: normalizedPath,
+ promise,
+ resolve: resolvePromise,
+ reject: rejectPromise,
+ });
+
+ // Return the promise that will be resolved when user makes a decision
+ return promise;
+}
+
+/**
+ * Resolve a pending permission request
+ * @param allowed Whether the permission was granted
+ */
+export function resolvePermission(allowed: boolean): void {
+ const store = getDefaultStore();
+ const currentRequest = store.get(pendingPermissionRequestAtom);
+
+ if (currentRequest) {
+ currentRequest.resolve(allowed);
+ store.set(pendingPermissionRequestAtom, null);
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/services/fileSystemSettings.ts b/apps/agent-tars/src/renderer/src/services/fileSystemSettings.ts
new file mode 100644
index 000000000..7e2822d12
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/services/fileSystemSettings.ts
@@ -0,0 +1,41 @@
+import { FileSystemSettings } from '@agent-infra/shared';
+
+const FILE_SYSTEM_SETTINGS_KEY = 'omega-filesystem-settings';
+
+export function loadFileSystemSettings(): FileSystemSettings | null {
+ try {
+ const settingsJson = localStorage.getItem(FILE_SYSTEM_SETTINGS_KEY);
+ if (!settingsJson) return null;
+ return JSON.parse(settingsJson) as FileSystemSettings;
+ } catch (error) {
+ console.error('Failed to load file system settings:', error);
+ return null;
+ }
+}
+
+export function saveFileSystemSettings(settings: FileSystemSettings): void {
+ try {
+ localStorage.setItem(FILE_SYSTEM_SETTINGS_KEY, JSON.stringify(settings));
+ } catch (error) {
+ console.error('Failed to save file system settings:', error);
+ }
+}
+
+export function getDefaultDirectory(): string | null {
+ const settings = loadFileSystemSettings();
+ if (settings && settings.availableDirectories.length > 0) {
+ return settings.availableDirectories[0];
+ }
+ return null;
+}
+
+export function isPathAllowed(path: string): boolean {
+ const settings = loadFileSystemSettings();
+ if (!settings || !settings.availableDirectories.length) return false;
+
+ // If path is not absolute, it's allowed (will be relative to default dir)
+ if (!path.startsWith('/')) return true;
+
+ // Check if the path is within any of the allowed directories
+ return settings.availableDirectories.some((dir) => path.startsWith(dir));
+}
diff --git a/apps/agent-tars/src/renderer/src/services/llmSettings.ts b/apps/agent-tars/src/renderer/src/services/llmSettings.ts
new file mode 100644
index 000000000..e559eebdc
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/services/llmSettings.ts
@@ -0,0 +1,43 @@
+import { ModelSettings } from '@agent-infra/shared';
+
+const STORAGE_KEY = 'ai-model-settings';
+
+/**
+ * Load LLM settings from localStorage
+ */
+export function loadLLMSettings(): ModelSettings | null {
+ try {
+ const savedSettings = localStorage.getItem(STORAGE_KEY);
+ if (savedSettings) {
+ return JSON.parse(savedSettings);
+ }
+ } catch (e) {
+ console.error('Failed to parse saved LLM settings', e);
+ }
+ return null;
+}
+
+/**
+ * Save LLM settings to localStorage
+ */
+export function saveLLMSettings(settings: ModelSettings): void {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
+ } catch (e) {
+ console.error('Failed to save LLM settings', e);
+ }
+}
+
+/**
+ * Get the current provider configuration based on settings
+ */
+export function getLLMProviderConfig(settings: ModelSettings) {
+ const { provider, model, apiKey, apiVersion, endpoint } = settings;
+ return {
+ configName: provider,
+ model,
+ apiKey,
+ apiVersion,
+ baseURL: endpoint,
+ };
+}
diff --git a/apps/agent-tars/src/renderer/src/services/searchSettings.ts b/apps/agent-tars/src/renderer/src/services/searchSettings.ts
new file mode 100644
index 000000000..9008c4ba0
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/services/searchSettings.ts
@@ -0,0 +1,22 @@
+import { SearchSettings } from '@agent-infra/shared';
+
+const SEARCH_SETTINGS_KEY = 'omega-search-settings';
+
+export function loadSearchSettings(): SearchSettings | null {
+ try {
+ const settingsJson = localStorage.getItem(SEARCH_SETTINGS_KEY);
+ if (!settingsJson) return null;
+ return JSON.parse(settingsJson) as SearchSettings;
+ } catch (error) {
+ console.error('Failed to load search settings:', error);
+ return null;
+ }
+}
+
+export function saveSearchSettings(settings: SearchSettings): void {
+ try {
+ localStorage.setItem(SEARCH_SETTINGS_KEY, JSON.stringify(settings));
+ } catch (error) {
+ console.error('Failed to save search settings:', error);
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/state/canvas.ts b/apps/agent-tars/src/renderer/src/state/canvas.ts
new file mode 100644
index 000000000..6aa39782f
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/state/canvas.ts
@@ -0,0 +1,82 @@
+import { CanvasDataSource } from '@renderer/type/canvas';
+import { atom, SetStateAction, WritableAtom } from 'jotai';
+
+class CanvasStateManager {
+ private baseState: WritableAtom<
+ {
+ isVisible: boolean;
+ dataSource: CanvasDataSource | null;
+ },
+ [
+ SetStateAction<{
+ isVisible: boolean;
+ dataSource: CanvasDataSource | null;
+ }>,
+ ],
+ void
+ >;
+
+ public readonly showCanvas: WritableAtom;
+ public readonly dataSource: WritableAtom<
+ CanvasDataSource | null,
+ [CanvasDataSource | null],
+ void
+ >;
+ public readonly updateState: WritableAtom<
+ null,
+ [
+ {
+ isVisible?: boolean;
+ dataSource?: CanvasDataSource | null;
+ },
+ ],
+ void
+ >;
+
+ constructor(
+ initialVisible = true,
+ initialDataSource: CanvasDataSource | null = null,
+ ) {
+ this.baseState = atom({
+ isVisible: initialVisible,
+ dataSource: initialDataSource,
+ });
+
+ this.showCanvas = atom(
+ (get) => get(this.baseState).isVisible,
+ (get, set, nextVisible: boolean) => {
+ const current = get(this.baseState);
+ set(this.baseState, { ...current, isVisible: nextVisible });
+ },
+ );
+
+ this.dataSource = atom(
+ (get) => get(this.baseState).dataSource,
+ (get, set, nextDataSource: CanvasDataSource | null) => {
+ const current = get(this.baseState);
+ set(this.baseState, { ...current, dataSource: nextDataSource });
+ },
+ );
+
+ this.updateState = atom(
+ null,
+ (
+ get,
+ set,
+ updates: { isVisible?: boolean; dataSource?: CanvasDataSource | null },
+ ) => {
+ const current = get(this.baseState);
+ set(this.baseState, {
+ ...current,
+ ...updates,
+ });
+ },
+ );
+ }
+}
+
+export const canvasStateManager = new CanvasStateManager();
+
+export const showCanvasAtom = canvasStateManager.showCanvas;
+export const canvasDataSourceAtom = canvasStateManager.dataSource;
+export const updateCanvasStateAtom = canvasStateManager.updateState;
diff --git a/apps/agent-tars/src/renderer/src/state/chat.ts b/apps/agent-tars/src/renderer/src/state/chat.ts
new file mode 100644
index 000000000..e3d8c5bad
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/state/chat.ts
@@ -0,0 +1,28 @@
+import { EventItem } from '@renderer/type/event';
+import { atom } from 'jotai';
+import EventEmitter from 'eventemitter3';
+import { PlanTask } from '@renderer/type/agent';
+
+export interface UserInterruptEvent {
+ type: 'user-interrupt';
+ text: string;
+}
+
+export interface TernimateEvent {
+ type: 'terminate';
+}
+
+export type GlobalEvent = UserInterruptEvent | TernimateEvent;
+
+export const eventsAtom = atom([]);
+
+export const currentEventIdAtom = atom(null);
+
+export const globalEventEmitter = new EventEmitter<{
+ [key: string]: (event: GlobalEvent) => void;
+}>();
+export const currentAgentFlowIdRefAtom = atom<{ current: string | null }>({
+ current: null,
+});
+export const agentStatusTipAtom = atom('');
+export const planTasksAtom = atom([]);
diff --git a/apps/agent-tars/src/renderer/src/type/agent.ts b/apps/agent-tars/src/renderer/src/type/agent.ts
new file mode 100644
index 000000000..0871e369c
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/type/agent.ts
@@ -0,0 +1,205 @@
+export enum PlanTaskStatus {
+ Todo = 'todo',
+ Doing = 'doing',
+ Done = 'done',
+ Skipped = 'skipped',
+}
+
+export interface PlanTask {
+ id: string;
+ title: string;
+ status: PlanTaskStatus;
+ startedAt?: number;
+ finishedAt?: number;
+ cost?: number;
+ error?: string;
+}
+
+export enum ActionStatus {
+ Success = 'success',
+ Failed = 'failed',
+ Running = 'running',
+}
+
+export enum ToolCallType {
+ ReadFile = 'read_file',
+ WriteFile = 'write_file',
+ ReadMultipleFiles = 'read_multiple_files',
+ EditFile = 'edit_file',
+ CreateDirectory = 'create_directory',
+ ListDirectory = 'list_directory',
+ DirectoryTree = 'directory_tree',
+ MoveFile = 'move_file',
+ SearchFiles = 'search_files',
+ GetFileInfo = 'get_file_info',
+ ListAllowedDirectories = 'list_allowed_directories',
+ RunCommand = 'run_command',
+ RunScript = 'run_script',
+ WebSearch = 'web_search',
+ BrowserNavigate = 'browser_navigate',
+ BrowserScreenshot = 'browser_screenshot',
+ BrowserClick = 'browser_click',
+ BrowserFormInputFill = 'browser_form_input_fill',
+ BrowserSelect = 'browser_select',
+ BrowserHover = 'browser_hover',
+ BrowserEvaluate = 'browser_evaluate',
+ BrowserGetHtml = 'browser_get_html',
+ BrowserGetText = 'browser_get_text',
+ BrowserReadLinks = 'browser_read_links',
+ BrowserScroll = 'browser_scroll',
+ BrowserNewTab = 'browser_new_tab',
+ BrowserCloseTab = 'browser_close_tab',
+ BrowserSwitchTab = 'browser_switch_tab',
+ ChatMessage = 'chat_message',
+}
+
+export interface ToolCallParam {
+ [ToolCallType.ReadFile]: {
+ path: string;
+ };
+ [ToolCallType.ChatMessage]: {
+ attachments?: { path: string }[];
+ };
+ [ToolCallType.WriteFile]: {
+ path: string;
+ content: string;
+ };
+ [ToolCallType.ReadMultipleFiles]: {
+ paths: string[];
+ };
+ [ToolCallType.EditFile]: {
+ path: string;
+ edits: Array<{
+ oldText: string;
+ newText: string;
+ }>;
+ dryRun?: boolean;
+ };
+ [ToolCallType.CreateDirectory]: {
+ path: string;
+ };
+ [ToolCallType.ListDirectory]: {
+ path: string;
+ };
+ [ToolCallType.DirectoryTree]: {
+ path: string;
+ };
+ [ToolCallType.MoveFile]: {
+ source: string;
+ destination: string;
+ };
+ [ToolCallType.SearchFiles]: {
+ path: string;
+ pattern: string;
+ excludePatterns?: string[];
+ };
+ [ToolCallType.GetFileInfo]: {
+ path: string;
+ };
+ [ToolCallType.ListAllowedDirectories]: Record;
+
+ [ToolCallType.RunCommand]: {
+ command: string;
+ cwd?: string;
+ };
+ [ToolCallType.RunScript]: {
+ interpreter: string;
+ script: string;
+ cwd?: string;
+ };
+ [ToolCallType.WebSearch]: {
+ query: string;
+ };
+ [ToolCallType.BrowserNavigate]: {
+ url: string;
+ };
+ [ToolCallType.BrowserScreenshot]: {
+ name: string;
+ selector?: string;
+ width?: number;
+ height?: number;
+ };
+ [ToolCallType.BrowserClick]: {
+ selector: string;
+ };
+ [ToolCallType.BrowserFormInputFill]: {
+ selector: string;
+ value: string;
+ };
+ [ToolCallType.BrowserSelect]: {
+ selector: string;
+ value: string;
+ };
+ [ToolCallType.BrowserHover]: {
+ selector: string;
+ };
+ [ToolCallType.BrowserEvaluate]: {
+ script: string;
+ };
+ [ToolCallType.BrowserGetHtml]: Record;
+ [ToolCallType.BrowserGetText]: Record;
+ [ToolCallType.BrowserReadLinks]: Record;
+ [ToolCallType.BrowserScroll]: {
+ amount: number;
+ };
+ [ToolCallType.BrowserNewTab]: {
+ url: string;
+ };
+ [ToolCallType.BrowserCloseTab]: Record;
+ [ToolCallType.BrowserSwitchTab]: {
+ index: number;
+ };
+}
+
+export interface ToolCallMeta {
+ type: ToolCallType;
+ param: ToolCallParam[ToolCallType];
+}
+
+export function isToolCallType(
+ toolName: string,
+ expectedType: T,
+): toolName is T {
+ return toolName === expectedType;
+}
+
+export type ToolCallParamFor = ToolCallParam[T];
+
+export enum ToolPlatform {
+ FileSystem = 'file system',
+ CommandLine = 'ternimal',
+ Search = 'search engine',
+ System = 'system',
+ Browser = 'browser',
+}
+
+export const toolToPlatformMap: Record = {
+ read_file: ToolPlatform.FileSystem,
+ write_file: ToolPlatform.FileSystem,
+ read_multiple_files: ToolPlatform.FileSystem,
+ edit_file: ToolPlatform.FileSystem,
+ create_directory: ToolPlatform.FileSystem,
+ list_directory: ToolPlatform.FileSystem,
+ directory_tree: ToolPlatform.FileSystem,
+ move_file: ToolPlatform.FileSystem,
+ search_files: ToolPlatform.FileSystem,
+ get_file_info: ToolPlatform.FileSystem,
+ list_allowed_directories: ToolPlatform.FileSystem,
+ run_command: ToolPlatform.CommandLine,
+ run_script: ToolPlatform.CommandLine,
+ web_search: ToolPlatform.Search,
+ browser_navigate: ToolPlatform.Browser,
+ browser_screenshot: ToolPlatform.Browser,
+ browser_click: ToolPlatform.Browser,
+ browser_form_input_fill: ToolPlatform.Browser,
+ browser_select: ToolPlatform.Browser,
+ browser_hover: ToolPlatform.Browser,
+ browser_evaluate: ToolPlatform.Browser,
+ browser_get_html: ToolPlatform.Browser,
+ browser_get_text: ToolPlatform.Browser,
+ browser_read_links: ToolPlatform.Browser,
+ browser_scroll: ToolPlatform.Browser,
+ browser_new_tab: ToolPlatform.Browser,
+ browser_close_tab: ToolPlatform.Browser,
+ browser_switch_tab: ToolPlatform.Browser,
+};
diff --git a/apps/agent-tars/src/renderer/src/type/canvas.ts b/apps/agent-tars/src/renderer/src/type/canvas.ts
new file mode 100644
index 000000000..894a1d0dc
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/type/canvas.ts
@@ -0,0 +1,24 @@
+export enum CanvasType {
+ EventPlayer = 'event-player',
+ ArtifactPreview = 'artifact-preview',
+}
+
+export enum ArtifactType {
+ Image = 'image',
+ Excel = 'excel',
+ PDF = 'pdf',
+ Component = 'component',
+}
+
+export interface CanvasDataSourceDescriptor {
+ [CanvasType.EventPlayer]: {};
+ [CanvasType.ArtifactPreview]: {
+ artifactType: ArtifactType;
+ content: string;
+ };
+}
+
+export interface CanvasDataSource {
+ type: CanvasType;
+ data: CanvasDataSourceDescriptor[CanvasType];
+}
diff --git a/apps/agent-tars/src/renderer/src/type/chatMessage.ts b/apps/agent-tars/src/renderer/src/type/chatMessage.ts
new file mode 100644
index 000000000..404c944f4
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/type/chatMessage.ts
@@ -0,0 +1,24 @@
+import {
+ MessageTypeDescriptor,
+ MessageItem as MessageItemBase,
+} from '@vendor/chat-ui';
+import { EventItem } from './event';
+
+export enum MessageType {
+ // Output the chat text directly
+ PlainText = 'plain-text',
+ // Display the file information
+ File = 'file',
+ // Output the agent workflow process
+ OmegaAgent = 'omega-agent',
+}
+
+export interface OmegaAgentData {
+ events: EventItem[];
+}
+
+export interface MessageContentType extends MessageTypeDescriptor {
+ [MessageType.OmegaAgent]: OmegaAgentData;
+}
+
+export type MessageItem = MessageItemBase;
diff --git a/apps/agent-tars/src/renderer/src/type/event.ts b/apps/agent-tars/src/renderer/src/type/event.ts
new file mode 100644
index 000000000..af12fd2e2
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/type/event.ts
@@ -0,0 +1,69 @@
+import { ActionStatus, PlanTask } from './agent';
+
+export enum EventType {
+ UserMessage = 'user-message',
+ LoadingStatus = 'loading-status',
+ ToolUsed = 'tool-used',
+ ToolCallStart = 'tool-call-start',
+ PlanUpdate = 'plan-update',
+ AgentStatus = 'agent-status',
+ ChatText = 'chat-text',
+ Observation = 'observation',
+ NewPlanStep = 'new-plan-step',
+ UserInteruption = 'user-interuption',
+ End = 'end',
+}
+
+export interface EventContentDescriptor {
+ [EventType.ChatText]: {
+ text: string;
+ attachments: { path: string }[];
+ };
+ [EventType.LoadingStatus]: {
+ title: string;
+ description?: string;
+ };
+ [EventType.PlanUpdate]: {
+ plan?: PlanTask[];
+ step?: number;
+ };
+ [EventType.ToolUsed]: {
+ actionId: string;
+ tool: string;
+ params: string;
+ status: ActionStatus;
+ description: string;
+ // Key param value in the tool params, such as the filepath or the command
+ value: string;
+ result: any;
+ // For displaying file content diff
+ original?: string;
+ };
+ [EventType.ToolCallStart]: {
+ tool: string;
+ params: string;
+ description: string;
+ // The same as above
+ value: string;
+ };
+ [EventType.AgentStatus]: string;
+ [EventType.NewPlanStep]: {
+ step: number;
+ };
+ // When tools return the result, it will be displayed as an observation
+ [EventType.Observation]: any;
+ [EventType.UserInteruption]: {
+ text: string;
+ };
+ [EventType.End]: {
+ message: string;
+ };
+ [EventType.UserMessage]: string;
+}
+
+export interface EventItem {
+ id: string;
+ content: EventContentDescriptor[keyof EventContentDescriptor];
+ type: EventType;
+ timestamp: number;
+}
diff --git a/apps/agent-tars/src/renderer/src/utils/ChatMessageUtils.tsx b/apps/agent-tars/src/renderer/src/utils/ChatMessageUtils.tsx
new file mode 100644
index 000000000..fd2006eee
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/utils/ChatMessageUtils.tsx
@@ -0,0 +1,57 @@
+import { MessageRole } from '@vendor/chat-ui';
+import {
+ MessageItem,
+ MessageType,
+ OmegaAgentData,
+} from '@renderer/type/chatMessage';
+
+export class ChatMessageUtil {
+ static userMessage(
+ content: string,
+ type = MessageType.PlainText,
+ ): MessageItem {
+ return {
+ role: MessageRole.User,
+ content,
+ type,
+ timestamp: Date.now(),
+ };
+ }
+
+ static assistantOmegaMessage(
+ content: OmegaAgentData,
+ type = MessageType.OmegaAgent,
+ ): MessageItem {
+ return {
+ role: MessageRole.Assistant,
+ content,
+ type,
+ timestamp: Date.now(),
+ showCopyButton: false,
+ };
+ }
+
+ static assistantTextMessage(content: string): MessageItem {
+ return {
+ role: MessageRole.Assistant,
+ content,
+ type: MessageType.PlainText,
+ timestamp: Date.now(),
+ showCopyButton: false,
+ // avatar: logo,
+ };
+ }
+
+ static assistantThinkMessage(
+ content: string,
+ type = MessageType.PlainText,
+ ): MessageItem {
+ return {
+ role: MessageRole.Assistant,
+ content,
+ type,
+ timestamp: Date.now(),
+ showCopyButton: false,
+ };
+ }
+}
diff --git a/apps/agent-tars/src/renderer/src/utils/extractHistoryEvents.ts b/apps/agent-tars/src/renderer/src/utils/extractHistoryEvents.ts
new file mode 100644
index 000000000..c758f1170
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/utils/extractHistoryEvents.ts
@@ -0,0 +1,14 @@
+import {
+ MessageContentType,
+ MessageItem,
+ MessageType,
+} from '@renderer/type/chatMessage';
+
+export function extractHistoryEvents(messages: MessageItem[]) {
+ return messages
+ .filter((message) => message.type === MessageType.OmegaAgent)
+ .flatMap(
+ (message) =>
+ (message.content as MessageContentType['omega-agent']).events,
+ );
+}
diff --git a/apps/agent-tars/src/renderer/src/utils/getLoadingTipForToolCall.ts b/apps/agent-tars/src/renderer/src/utils/getLoadingTipForToolCall.ts
new file mode 100644
index 000000000..a6bd2a6a9
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/utils/getLoadingTipForToolCall.ts
@@ -0,0 +1,183 @@
+import {
+ ActionStatus,
+ ToolCallParam,
+ ToolCallType,
+} from '@renderer/type/agent';
+
+interface LoadingTipMeta {
+ name: string;
+ value: string;
+ description: string;
+}
+
+export function getLoadingTipFromToolCall(
+ tool: string,
+ toolParams: string,
+ status: ActionStatus,
+): LoadingTipMeta {
+ const toolName = tool as ToolCallType;
+ const params = JSON.parse(toolParams || '{}');
+
+ let value = '';
+ let actionStatus = '';
+ switch (status) {
+ case ActionStatus.Running:
+ actionStatus = 'Executing';
+ break;
+ case ActionStatus.Success:
+ actionStatus = 'Executed';
+ break;
+ case ActionStatus.Failed:
+ actionStatus = 'Failed';
+ break;
+ }
+
+ switch (toolName) {
+ case ToolCallType.ReadFile: {
+ const typedParams = params as ToolCallParam[ToolCallType.ReadFile];
+ value = typedParams.path;
+ break;
+ }
+ case ToolCallType.WriteFile: {
+ const typedParams = params as ToolCallParam[ToolCallType.WriteFile];
+ value = typedParams.path;
+ break;
+ }
+ case ToolCallType.GetFileInfo: {
+ const typedParams = params as ToolCallParam[ToolCallType.GetFileInfo];
+ value = typedParams.path;
+ break;
+ }
+ case ToolCallType.ReadMultipleFiles: {
+ const typedParams =
+ params as ToolCallParam[ToolCallType.ReadMultipleFiles];
+ value = `${typedParams.paths.length} files`;
+ break;
+ }
+ case ToolCallType.EditFile: {
+ const typedParams = params as ToolCallParam[ToolCallType.EditFile];
+ value = `${typedParams.path} (${typedParams.edits.length} edits)`;
+ break;
+ }
+ case ToolCallType.CreateDirectory: {
+ const typedParams = params as ToolCallParam[ToolCallType.CreateDirectory];
+ value = typedParams.path;
+ break;
+ }
+ case ToolCallType.ListDirectory: {
+ const typedParams = params as ToolCallParam[ToolCallType.ListDirectory];
+ value = typedParams.path;
+ break;
+ }
+ case ToolCallType.DirectoryTree: {
+ const typedParams = params as ToolCallParam[ToolCallType.DirectoryTree];
+ value = typedParams.path;
+ break;
+ }
+ case ToolCallType.MoveFile: {
+ const typedParams = params as ToolCallParam[ToolCallType.MoveFile];
+ value = `${typedParams.source} → ${typedParams.destination}`;
+ break;
+ }
+ case ToolCallType.SearchFiles: {
+ const typedParams = params as ToolCallParam[ToolCallType.SearchFiles];
+ value = `${typedParams.path} (pattern: ${typedParams.pattern})`;
+ break;
+ }
+ case ToolCallType.RunCommand: {
+ const typedParams = params as ToolCallParam[ToolCallType.RunCommand];
+ value = typedParams.command;
+ break;
+ }
+ case ToolCallType.RunScript: {
+ const typedParams = params as ToolCallParam[ToolCallType.RunScript];
+ value = `${typedParams.interpreter} script`;
+ break;
+ }
+ case ToolCallType.WebSearch: {
+ const typedParams = params as ToolCallParam[ToolCallType.WebSearch];
+ value = typedParams.query;
+ break;
+ }
+ case ToolCallType.BrowserNavigate: {
+ const typedParams = params as ToolCallParam[ToolCallType.BrowserNavigate];
+ value = typedParams.url;
+ break;
+ }
+ case ToolCallType.BrowserScreenshot: {
+ const typedParams =
+ params as ToolCallParam[ToolCallType.BrowserScreenshot];
+ value = `${typedParams.name}${typedParams.selector ? ` (${typedParams.selector})` : ''}`;
+ break;
+ }
+ case ToolCallType.BrowserClick: {
+ const typedParams = params as ToolCallParam[ToolCallType.BrowserClick];
+ value = typedParams.selector;
+ break;
+ }
+ case ToolCallType.BrowserFormInputFill: {
+ const typedParams =
+ params as ToolCallParam[ToolCallType.BrowserFormInputFill];
+ value = `${typedParams.selector} → ${typedParams.value}`;
+ break;
+ }
+ case ToolCallType.BrowserSelect: {
+ const typedParams = params as ToolCallParam[ToolCallType.BrowserSelect];
+ value = `${typedParams.selector} → ${typedParams.value}`;
+ break;
+ }
+ case ToolCallType.BrowserHover: {
+ const typedParams = params as ToolCallParam[ToolCallType.BrowserHover];
+ value = typedParams.selector;
+ break;
+ }
+ case ToolCallType.BrowserEvaluate: {
+ value = 'executing script...';
+ break;
+ }
+ case ToolCallType.BrowserGetHtml:
+ case ToolCallType.BrowserGetText:
+ case ToolCallType.BrowserReadLinks: {
+ value = 'fetching...';
+ break;
+ }
+ case ToolCallType.BrowserScroll: {
+ const typedParams = params as ToolCallParam[ToolCallType.BrowserScroll];
+ value = `${typedParams.amount}px`;
+ break;
+ }
+ case ToolCallType.BrowserNewTab: {
+ const typedParams = params as ToolCallParam[ToolCallType.BrowserNewTab];
+ value = typedParams.url;
+ break;
+ }
+ case ToolCallType.BrowserCloseTab: {
+ value = 'closing current tab';
+ break;
+ }
+ case ToolCallType.BrowserSwitchTab: {
+ const typedParams =
+ params as ToolCallParam[ToolCallType.BrowserSwitchTab];
+ value = `tab ${typedParams.index}`;
+ break;
+ }
+ default:
+ switch (status) {
+ case ActionStatus.Running:
+ value = 'executing...';
+ break;
+ case ActionStatus.Success:
+ value = 'done';
+ break;
+ case ActionStatus.Failed:
+ value = 'failed';
+ break;
+ }
+ }
+
+ return {
+ name: toolName,
+ description: `${actionStatus} ${toolName?.replace(/_/g, ' ')}...`,
+ value,
+ };
+}
diff --git a/apps/agent-tars/src/renderer/src/utils/index.ts b/apps/agent-tars/src/renderer/src/utils/index.ts
new file mode 100644
index 000000000..9ad0df426
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/utils/index.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/agent-tars/src/renderer/src/utils/monacoConfig.ts b/apps/agent-tars/src/renderer/src/utils/monacoConfig.ts
new file mode 100644
index 000000000..95ad8883c
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/utils/monacoConfig.ts
@@ -0,0 +1,40 @@
+import * as monaco from 'monaco-editor';
+import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
+import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
+import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
+import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
+import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
+
+export function initMonacoWorkers(): void {
+ self.MonacoEnvironment = {
+ getWorker(_, label) {
+ if (label === 'json') {
+ return new jsonWorker();
+ }
+ if (label === 'css' || label === 'scss' || label === 'less') {
+ return new cssWorker();
+ }
+ if (label === 'html' || label === 'handlebars' || label === 'razor') {
+ return new htmlWorker();
+ }
+ if (label === 'typescript' || label === 'javascript') {
+ return new tsWorker();
+ }
+ return new editorWorker();
+ },
+ };
+}
+
+export const defaultEditorOptions: monaco.editor.IStandaloneEditorConstructionOptions =
+ {
+ readOnly: true,
+ minimap: { enabled: false },
+ scrollBeyondLastLine: false,
+ fontSize: 14,
+ renderLineHighlight: 'all',
+ scrollbar: {
+ vertical: 'visible',
+ horizontal: 'visible',
+ },
+ automaticLayout: true,
+ };
diff --git a/apps/agent-tars/src/renderer/src/utils/normalizeToolUsedInfo.ts b/apps/agent-tars/src/renderer/src/utils/normalizeToolUsedInfo.ts
new file mode 100644
index 000000000..41a88a187
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/utils/normalizeToolUsedInfo.ts
@@ -0,0 +1,27 @@
+import {
+ ActionStatus,
+ ToolPlatform,
+ toolToPlatformMap,
+} from '@renderer/type/agent';
+import { getLoadingTipFromToolCall } from './getLoadingTipForToolCall';
+
+export function normalizeToolUsedInfo(
+ tool: string,
+ params: string,
+ status: ActionStatus,
+ result: {
+ [key: string]: any;
+ },
+) {
+ const platform = toolToPlatformMap[tool] || ToolPlatform.System;
+ const description = `Using ${platform} to execute ${tool.replace(/_/g, ' ')}`;
+ const { value } = getLoadingTipFromToolCall(tool, params, status);
+ return {
+ tool,
+ params,
+ description,
+ status,
+ value,
+ result,
+ };
+}
diff --git a/apps/agent-tars/src/renderer/src/utils/parseEvents.test.ts b/apps/agent-tars/src/renderer/src/utils/parseEvents.test.ts
new file mode 100644
index 000000000..96335f137
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/utils/parseEvents.test.ts
@@ -0,0 +1,81 @@
+import { describe, it, expect } from 'vitest';
+import { groupEventsByStep, UIGroupType } from './parseEvents';
+import { EventType, EventItem } from '@renderer/type/event';
+
+const createEvent = (type: EventType, content: any): EventItem => ({
+ id: `test-${type}-${Date.now()}`,
+ type,
+ content,
+ timestamp: Date.now(),
+});
+
+describe('parseEvents', () => {
+ describe('groupEventsByStep', () => {
+ it('should handle empty events array', () => {
+ expect(groupEventsByStep([])).toEqual([]);
+ });
+
+ it('should group loading events before first plan update', () => {
+ const events = [
+ createEvent(EventType.LoadingStatus, { title: 'Loading 1' }),
+ createEvent(EventType.PlanUpdate, { tasks: [] }),
+ ];
+
+ const result = groupEventsByStep(events);
+ expect(result).toHaveLength(1);
+ expect(result[0].type).toBe(UIGroupType.Loading);
+ });
+
+ it('should group events by plan steps', () => {
+ const events = [
+ createEvent(EventType.PlanUpdate, { tasks: [] }),
+ createEvent(EventType.NewPlanStep, { step: 1 }),
+ createEvent(EventType.ToolUsed, { tool: 'test' }),
+ createEvent(EventType.ToolUsed, { tool: 'test2' }),
+ createEvent(EventType.ChatText, 'Hello'),
+ ];
+
+ const result = groupEventsByStep(events);
+ expect(result).toHaveLength(2);
+ expect(result[0].type).toBe(UIGroupType.PlanStep);
+ expect(result[1].type).toBe(UIGroupType.ChatText);
+ });
+
+ it('should handle chat text as separate groups', () => {
+ const events = [
+ createEvent(EventType.PlanUpdate, { tasks: [] }),
+ createEvent(EventType.ChatText, 'Message 1'),
+ createEvent(EventType.ChatText, 'Message 2'),
+ ];
+
+ const result = groupEventsByStep(events);
+ expect(result).toHaveLength(2);
+ expect(result[0].type).toBe(UIGroupType.ChatText);
+ expect(result[1].type).toBe(UIGroupType.ChatText);
+ });
+
+ it('should only include last loading status', () => {
+ const events = [
+ createEvent(EventType.PlanUpdate, { tasks: [] }),
+ createEvent(EventType.LoadingStatus, { title: 'Loading 1' }),
+ createEvent(EventType.LoadingStatus, { title: 'Loading 2' }),
+ ];
+
+ const result = groupEventsByStep(events);
+ expect(result).toHaveLength(1);
+ expect(result[0].events[0].content.title).toBe('Loading 2');
+ });
+
+ it('should work at the beginning', () => {
+ const events = [
+ createEvent(EventType.LoadingStatus, { title: 'Loading 1' }),
+ createEvent(EventType.PlanUpdate, 'Message 1'),
+ createEvent(EventType.NewPlanStep, { step: 1 }),
+ ];
+
+ const result = groupEventsByStep(events);
+ expect(result).toHaveLength(1);
+ expect(result[0].type).toBe(UIGroupType.Loading);
+ });
+ });
+});
diff --git a/apps/agent-tars/src/renderer/src/utils/parseEvents.ts b/apps/agent-tars/src/renderer/src/utils/parseEvents.ts
new file mode 100644
index 000000000..a6bf57fa4
--- /dev/null
+++ b/apps/agent-tars/src/renderer/src/utils/parseEvents.ts
@@ -0,0 +1,224 @@
+import { PlanTask, PlanTaskStatus } from '@renderer/type/agent';
+import { EventItem, EventType } from '@renderer/type/event';
+
+export enum UIGroupType {
+ ChatText = 'chat-text',
+ PlanStep = 'plan-step',
+ Loading = 'loading',
+ End = 'end',
+}
+
+export interface UIGroup {
+ type: UIGroupType;
+ step: number;
+ events: EventItem[];
+}
+
+export interface EventStreamUIMeta {
+ planTasks: PlanTask[];
+ agentStatus: string;
+ currentStepIndex: number;
+ currentEvent: EventItem;
+ eventGroups: UIGroup[];
+ isLoading: boolean;
+}
+
+/**
+ * Extracts UI metadata from events stream
+ */
+export function extractEventStreamUIMeta(
+ events: EventItem[],
+): EventStreamUIMeta {
+ // Get latest plan tasks
+ const lastPlanUpdate = [...events]
+ .reverse()
+ .find((event) => event.type === EventType.PlanUpdate && event.content.plan);
+ const planTasks = lastPlanUpdate
+ ? (lastPlanUpdate.content as { plan: PlanTask[] }).plan
+ : [];
+
+ // Get latest agent status
+ const lastAgentStatus = [...events]
+ .reverse()
+ .find((event) => event.type === EventType.AgentStatus);
+ const agentStatus = lastAgentStatus
+ ? (lastAgentStatus.content as string)
+ : '';
+
+ // Get current step
+ const lastStepEvent = [...events]
+ .reverse()
+ .find((event) => event.type === EventType.NewPlanStep);
+ const currentStepIndex = lastStepEvent
+ ? (lastStepEvent.content as { step: number }).step
+ : 1;
+
+ // Get latest non-loading event
+ const lastEvent = events[events.length - 1];
+ const isLoading = lastEvent?.type === EventType.LoadingStatus;
+ const eventGroups = groupEventsByStep(events);
+ return {
+ planTasks,
+ agentStatus,
+ currentStepIndex,
+ currentEvent: lastEvent,
+ isLoading,
+ eventGroups,
+ };
+}
+
+/**
+ * Groups events by their type (ChatText vs PlanStep)
+ */
+export function groupEventsByStep(events: EventItem[]): UIGroup[] {
+ const groups: UIGroup[] = [];
+ let currentStepEvents: EventItem[] = [];
+ let hasPlan = false;
+ const NO_RENDER_TYPE = [
+ EventType.PlanUpdate,
+ EventType.Observation,
+ EventType.ToolCallStart,
+ ];
+ let currentStep = 1;
+
+ const filterLoading = (pendingEvents: EventItem[]) => {
+ let clonedEvents = [...pendingEvents];
+ let lastEvent = clonedEvents[clonedEvents.length - 1];
+ const tailingNoRenderEvents: EventItem[] = [];
+ if (lastEvent && NO_RENDER_TYPE.includes(lastEvent.type)) {
+ clonedEvents = clonedEvents.slice(0, -1);
+ tailingNoRenderEvents.unshift(lastEvent);
+ lastEvent = clonedEvents[clonedEvents.length - 1];
+ }
+ return [
+ ...clonedEvents.filter((item, index) => {
+ if (
+ index < clonedEvents.length - 1 &&
+ item.type === EventType.LoadingStatus
+ ) {
+ return false;
+ }
+ return true;
+ }),
+ ...tailingNoRenderEvents,
+ ];
+ };
+ console.log('filtered events', filterLoading(events));
+
+ filterLoading(events).forEach((event) => {
+ if (event.type === EventType.PlanUpdate) {
+ hasPlan = true;
+ const allDone = event.content.plan.every(
+ (task) => task.status === PlanTaskStatus.Done,
+ );
+ if (allDone) {
+ // No need to render new plan step
+ return;
+ }
+ currentStep = (event.content as { step: number | undefined }).step || 1;
+
+ if (currentStepEvents.length > 0) {
+ const lastGroup = groups[groups.length - 1];
+ if (
+ lastGroup &&
+ lastGroup.type === UIGroupType.PlanStep &&
+ lastGroup.step <= currentStep
+ ) {
+ lastGroup.events.push(...currentStepEvents);
+ if (lastGroup.step < currentStep) {
+ groups.push({
+ type: UIGroupType.PlanStep,
+ step: currentStep,
+ events: [],
+ });
+ }
+ } else {
+ groups.push({
+ type: UIGroupType.PlanStep,
+ step: currentStep,
+ events: [...currentStepEvents],
+ });
+ }
+ currentStepEvents = [];
+ } else {
+ groups.push({
+ type: UIGroupType.PlanStep,
+ step: currentStep,
+ events: [],
+ });
+ }
+ return;
+ }
+
+ // The last event
+ if (event.type === EventType.LoadingStatus) {
+ if (hasPlan) {
+ // loading in plan step
+ currentStepEvents.push(event);
+ } else {
+ // Initial loading
+ groups.push({
+ type: UIGroupType.Loading,
+ step: 1,
+ events: [event],
+ });
+ }
+ return;
+ }
+
+ if (event.type === EventType.ToolCallStart) {
+ return;
+ }
+
+ if (event.type === EventType.ChatText || event.type === EventType.End) {
+ if (currentStepEvents.length > 0) {
+ const lastGroup = groups[groups.length - 1];
+ if (lastGroup && lastGroup.step === currentStep) {
+ lastGroup.events.push(...currentStepEvents);
+ currentStepEvents = [];
+ }
+ }
+ const groupType = (() => {
+ if (event.type === EventType.End) {
+ return UIGroupType.End;
+ }
+ return UIGroupType.ChatText;
+ })();
+ groups.push({
+ type: groupType,
+ step: currentStep,
+ events: [event],
+ });
+ return;
+ }
+
+ currentStepEvents.push(event);
+ });
+
+ if (currentStepEvents.length > 0) {
+ if (groups.length > 0) {
+ const lastGroup = groups[groups.length - 1];
+ if (lastGroup.step === currentStep) {
+ if (lastGroup.type === UIGroupType.PlanStep) {
+ lastGroup.events.push(...currentStepEvents);
+ } else {
+ groups.push({
+ type: UIGroupType.PlanStep,
+ step: currentStep,
+ events: [...currentStepEvents],
+ });
+ }
+ return groups;
+ }
+ }
+ groups.push({
+ type: UIGroupType.PlanStep,
+ step: currentStep,
+ events: [...currentStepEvents],
+ });
+ }
+
+ console.log('groups', groups);
+
+ return groups;
+}
diff --git a/apps/agent-tars/src/vendor/chat-ui/index.d.ts b/apps/agent-tars/src/vendor/chat-ui/index.d.ts
new file mode 100644
index 000000000..0d12f6866
--- /dev/null
+++ b/apps/agent-tars/src/vendor/chat-ui/index.d.ts
@@ -0,0 +1,592 @@
+import { atom } from 'jotai';
+import * as React$1 from 'react';
+import React__default from 'react';
+
+declare enum MessageRole {
+ User = 'user',
+ Assistant = 'assistant',
+}
+declare enum MessageType {
+ PlainText = 'plain-text',
+ File = 'file',
+}
+interface MessageTypeDescriptor {
+ [MessageType.PlainText]: string;
+ [MessageType.File]: InputFile;
+ [key: string]: unknown;
+}
+interface MessageItem {
+ id?: string;
+ role: MessageRole;
+ avatar?: string;
+ conversationId?: string;
+ timestamp?: number;
+ isFinal?: boolean;
+ type: keyof T;
+ content?: T[keyof T];
+ isArchived?: boolean;
+ isWelcome?: boolean;
+ showCopyButton?: boolean;
+ isDeleting?: boolean;
+}
+declare enum InputFileType {
+ Image = 'image',
+ PDF = 'pdf',
+ Text = 'text',
+ Json = 'json',
+ Zip = 'zip',
+ Audio = 'audio',
+ Video = 'video',
+ Keynote = 'keynote',
+ Powerpoint = 'powerpoint',
+ Excel = 'excel',
+ Word = 'word',
+ PPTX = 'pptx',
+ XLSX = 'xlsx',
+ DOCX = 'docx',
+ Other = 'other',
+ JS = 'js',
+ TS = 'ts',
+ JSX = 'jsx',
+ TSX = 'tsx',
+ HTML = 'html',
+ CSS = 'css',
+ SCSS = 'scss',
+ LESS = 'less',
+ YAML = 'yaml',
+ XML = 'xml',
+ TOML = 'toml',
+ Python = 'py',
+ Java = 'java',
+ Rust = 'rs',
+ Swift = 'swift',
+ Go = 'go',
+ C = 'c',
+ CPP = 'cpp',
+ Stylus = 'stylus',
+ PHP = 'php',
+ Ruby = 'rb',
+ Kotlin = 'kt',
+ CSharp = 'cs',
+ Draft = 'draft',
+}
+interface InputFile {
+ type: InputFileType;
+ filename?: string;
+ content: string;
+ size?: number;
+ loading?: boolean;
+ isScreenshot?: boolean;
+ originalFile?: File;
+}
+
+interface ChatStates {
+ inputTextAtom: ReturnType>;
+ inputFilesAtom: ReturnType>;
+ messagesAtom: ReturnType[]>>;
+ messageLoadingAtom: ReturnType>;
+ imageLoadingAtom: ReturnType>;
+ uploadMessagePromiseRefAtom: ReturnType<
+ typeof atom<{
+ current: Promise | null> | null;
+ }>
+ >;
+ messageEndRefAtom: ReturnType<
+ typeof atom<{
+ current: HTMLDivElement | null;
+ }>
+ >;
+ messageSendingAtom: ReturnType>;
+ isUserScrollingRefAtom: ReturnType<
+ typeof atom<{
+ current: boolean;
+ }>
+ >;
+}
+
+interface ChatUISlots {
+ /**
+ * The slot before input section
+ */
+ beforeInputContainer?: React.ReactNode;
+ /**
+ * The slot after the input section
+ */
+ afterInputContainer?: React.ReactNode;
+ /**
+ * The slot before the input area
+ */
+ beforeInputArea?: React.ReactNode;
+ /**
+ * The slot after the input area
+ */
+ afterInputArea?: React.ReactNode;
+ /**
+ * The slot before the message list
+ */
+ beforeMessageList?: React.ReactNode;
+ /**
+ * The slot after the message list
+ */
+ afterMessageList?: React.ReactNode;
+ /**
+ * The slot before the message
+ */
+ beforeMessage?: React.ReactNode;
+ /**
+ * The slot after the message
+ */
+ afterMessage?: React.ReactNode;
+ /**
+ * The custom feature buttons
+ */
+ customFeatures?: React.ReactNode;
+ /**
+ * Secondary feature buttons
+ */
+ secondaryFeatures?: React.ReactNode;
+ /**
+ * Custom send button area
+ */
+ customSendButton?: React.ReactNode;
+ /**
+ * custom loading
+ */
+ customLoading?: React.ReactNode;
+ /**
+ * The custom message action
+ */
+ renderCustomMessageActionUI?: (message: MessageItem) => React.ReactNode;
+}
+interface ChatUIFeatures {
+ clearConversationHistory?: boolean;
+ uploadFiles?: boolean;
+}
+interface Avatar {
+ user?: React.ReactNode | true;
+ assistant?: React.ReactNode | true;
+}
+interface CommandPanelDataItem {
+ icon?: React.ReactNode;
+ /**
+ * The name for parse result
+ */
+ name: string;
+ /**
+ * The text in select panel
+ */
+ selectText?: string;
+ /**
+ * The label `@xxx` in input
+ */
+ label: string;
+ /**
+ * The description for item
+ */
+ description?: string;
+ /**
+ * The items in select panel
+ */
+ items?: CommandPanelDataItem[];
+}
+interface CommandPanelConfigValue {
+ /**
+ * The name of command
+ */
+ name: string;
+ /**
+ * The panel title
+ */
+ data: CommandPanelDataItem[];
+}
+interface CommandPanelConfig {
+ [key: string]: CommandPanelConfigValue;
+}
+interface ChatUIProps {
+ /**
+ * The panel config after user input `@`
+ */
+ commandPanelConfig?: CommandPanelConfig;
+ /**
+ * The necessary state and setState method in chat ui
+ */
+ states?: ChatStates;
+ /**
+ * The slots for different ui block
+ */
+ slots?: ChatUISlots;
+ /**
+ * Features in chat input
+ */
+ features?: ChatUIFeatures;
+ /**
+ * The callback implement for send message bahaviour
+ */
+ onMessageSend?: (
+ inputText: string,
+ inputFiles: InputFile[],
+ ) => void | Promise;
+ /**
+ * The callback implement for abort message bahaviour
+ */
+ onMessageAbort?: () => void;
+ /**
+ * The callback implement for command panel select
+ */
+ onCommandTrigger?: (command: string) => void;
+ /**
+ * The callback implement for command panel select
+ */
+ onCommandSelect?: (command: string, item: CommandPanelDataItem) => void;
+ /**
+ * The callback implement for command panel delete
+ */
+ onCommandDelete?: (command: string, item: CommandPanelDataItem) => void;
+ /**
+ * The callback implement for command panel input
+ */
+ onSuggestionClick?: (suggestion: string) => void | Promise;
+ /**
+ * The callback implement for clear conversation history
+ */
+ onClearConversationHistory?: () => void;
+ /**
+ * The custom style settings for different ui block
+ */
+ styles?: {
+ message?: React.CSSProperties;
+ messageList?: React.CSSProperties;
+ input?: React.CSSProperties;
+ inputContainer?: React.CSSProperties;
+ container?: React.CSSProperties;
+ };
+ /**
+ * The custom class name for different ui block
+ */
+ classNames?: {
+ message?: string;
+ messageList?: string;
+ input?: string;
+ inputContainer?: string;
+ container?: string;
+ };
+ /**
+ * The welcome message
+ */
+ welcomeMessage?: MessageItem;
+ /**
+ * Whether to disable input
+ */
+ disableInput?: boolean;
+ /**
+ * The conversation id for persistence
+ */
+ conversationId?: string;
+ /**
+ * Storage database name for message storage to identify different chats
+ */
+ storageDbName?: string;
+ /**
+ * Persistent message storage implement
+ */
+ customMessageStorage?: MessageStorage;
+ /**
+ * Integrate custom render method for message in different types
+ */
+ customMessageRender?: (message: MessageItem) => React.ReactNode;
+ /**
+ * Whether to auto scroll to bottom
+ */
+ autoScrollToBottom?: boolean;
+ /**
+ * Placeholder for input
+ */
+ inputPlaceholder?: string;
+ /**
+ * Whether to enable dark mode
+ */
+ isDark?: boolean;
+ /**
+ * The suggestions for user
+ */
+ suggestions?: string[];
+ /**
+ * Avatar for user and assistant
+ */
+ avatar?: Avatar;
+}
+
+interface MessageStorage<
+ T extends MessageTypeDescriptor = MessageTypeDescriptor,
+> {
+ getMessages: (appId: string) => Promise[]> | MessageItem[];
+ uploadMessage: (
+ message: MessageItem,
+ ) => Promise | undefined> | MessageItem | undefined;
+ updateMessage: (
+ message: Partial>,
+ ) => Promise | undefined> | MessageItem | undefined;
+ clearMessages: (appId: string) => Promise | void;
+ deleteMessage: (messageId: string) => Promise | void;
+}
+
+declare class DefaultMessageStorage
+ implements MessageStorage
+{
+ private store;
+ private cache;
+ id: number;
+ constructor(dbName: string);
+ private getMessageIds;
+ getStore(): LocalForage;
+ private setMessageIds;
+ getMessages(conversationId: string): Promise[]>;
+ uploadMessage(message: MessageItem): Promise | undefined>;
+ updateMessage(
+ message: Partial>,
+ ): Promise | undefined>;
+ clearMessages(conversationId: string): Promise;
+ deleteMessage(messageId: string): Promise;
+}
+
+interface ChatOptions {
+ states?: ChatStates;
+ conversationId?: string;
+ storageDbName?: string;
+ customMessageStorage?: MessageStorage;
+}
+declare function useChat({
+ states,
+ storageDbName,
+ customMessageStorage: messageStorage,
+ conversationId,
+}?: ChatOptions): {
+ addMessage: (
+ rawMessage: MessageItem,
+ options?: {
+ shouldSyncStorage?: boolean;
+ },
+ ) => Promise | null>;
+ updateMessage: (
+ messageInfo: Partial>,
+ options?: {
+ shouldSyncStorage?: boolean;
+ messageId?: string;
+ shouldScrollToBottom?: boolean;
+ },
+ ) => Promise;
+ messages: MessageItem[];
+ setMessages: (
+ args_0: MessageItem[] | ((prev: MessageItem[]) => MessageItem[]),
+ ) => void;
+ initMessages: () => Promise<
+ | {
+ isFinal: boolean;
+ id?: string;
+ role: MessageRole;
+ avatar?: string;
+ conversationId?: string;
+ timestamp?: number;
+ type: keyof T;
+ content?: T[keyof T] | undefined;
+ isArchived?: boolean;
+ isWelcome?: boolean;
+ showCopyButton?: boolean;
+ isDeleting?: boolean;
+ }[]
+ | undefined
+ >;
+ clearMessages: () => Promise;
+ deleteLastMessage: ({
+ deleteUIState,
+ deleteStorageRecord,
+ }: {
+ deleteUIState?: boolean;
+ deleteStorageRecord?: boolean;
+ }) => Promise;
+ deleteMessage: (messageId: string) => Promise;
+ messageLoading: boolean;
+ setMessageLoading: (args_0: boolean | ((prev: boolean) => boolean)) => void;
+ imageLoading: boolean;
+ setImageLoading: (args_0: boolean | ((prev: boolean) => boolean)) => void;
+ inputText: string;
+ setInputText: (args_0: string | ((prev: string) => string)) => void;
+ inputFiles: InputFile[];
+ setInputFiles: (
+ args_0: InputFile[] | ((prev: InputFile[]) => InputFile[]),
+ ) => void;
+ messageSending: boolean;
+ setMessageSending: (args_0: boolean | ((prev: boolean) => boolean)) => void;
+ messageEndRef: {
+ current: HTMLDivElement | null;
+ };
+};
+
+declare function createChatStateAtoms<
+ T extends MessageTypeDescriptor,
+>(): ChatStates;
+
+interface CopyButtonProps {
+ getCopyText: () => string;
+ tooltipText?: string;
+ successText?: string;
+}
+declare const CopyButton: React__default.FC;
+
+type MessageContent =
+ | string
+ | (
+ | {
+ type: 'image_url';
+ image_url: {
+ url: string;
+ };
+ }
+ | {
+ type: 'text';
+ text: string;
+ }
+ | string
+ )[];
+interface PromptMessage {
+ role: string;
+ content: MessageContent;
+}
+declare const getFileType: (fileName: string) => InputFileType;
+declare function getFileIcon(
+ fileName: string,
+ size?: number,
+): React__default.JSX.Element;
+declare function normalizeFilesPrompt(
+ inputFiles: InputFile[],
+): Promise;
+declare function normalizeFiles(inputFiles: InputFile[]): Promise;
+
+declare const handleImage: (
+ file: File,
+ setInputFiles: React.Dispatch>,
+) => Promise;
+declare const handleFileInput: (
+ file: File,
+ setInputFiles: React.Dispatch>,
+) => Promise;
+
+declare function FileItemPreview({
+ file,
+ handleDelete,
+ showDeleteIcon,
+ itemStyle,
+}: {
+ file: InputFile;
+ handleDelete?: () => void;
+ showDeleteIcon?: boolean;
+ itemStyle?: React__default.CSSProperties;
+}): React__default.JSX.Element;
+declare function FilePreview({
+ files,
+ handleDelete,
+ showDeleteIcon,
+ containerStyle,
+ itemStyle,
+}: {
+ files: InputFile[];
+ handleDelete?: (index: number) => void;
+ showDeleteIcon?: boolean;
+ containerStyle?: React__default.CSSProperties;
+ itemStyle?: React__default.CSSProperties;
+}): React__default.JSX.Element;
+
+interface ParsedItem {
+ type: 'commandText' | 'text';
+ content: string;
+}
+declare function parseCommandRules(
+ text: string,
+ config: CommandPanelConfig,
+): ParsedItem[];
+
+declare function CommandPanel({
+ config,
+ isShow,
+ setIsShow,
+ inputRef,
+ filterText,
+ setInputText,
+ setFilterText,
+ command,
+ onSelect,
+ isLoading,
+}: {
+ config: CommandPanelConfigValue;
+ isShow: boolean;
+ command: string;
+ setIsShow: (isShow: boolean) => void;
+ inputRef: React.RefObject;
+ filterText: string;
+ setInputText: (text: string) => void;
+ setFilterText: React.Dispatch>;
+ onSelect?: (cmd: string, item: CommandPanelDataItem) => void;
+ isLoading: boolean;
+}): React$1.JSX.Element;
+
+interface IconProps {
+ size?: number;
+ className?: string;
+}
+
+declare function FigmaIcon({ size, className }: IconProps): React$1.JSX.Element;
+
+interface ChatUIRef {
+ triggerCommand: (command: string) => void;
+ focusInput: () => void;
+ getInputTextArea: () => HTMLTextAreaElement | null | undefined;
+}
+declare const ChatUIComp: (
+ props: ChatUIProps & {
+ ref?: React__default.Ref;
+ },
+) => React__default.ReactElement;
+declare const ChatUI: typeof ChatUIComp;
+
+interface MarkdownRendererProps {
+ content: string;
+ isDark?: boolean;
+ smooth?: boolean;
+ streamDelay?: number;
+}
+declare const MarkdownRenderer: React__default.FC;
+
+export {
+ type Avatar,
+ type ChatStates,
+ ChatUI,
+ type ChatUIFeatures,
+ type ChatUIProps,
+ type ChatUISlots,
+ CommandPanel,
+ type CommandPanelConfig,
+ type CommandPanelConfigValue,
+ type CommandPanelDataItem,
+ CopyButton,
+ DefaultMessageStorage,
+ FigmaIcon,
+ FileItemPreview,
+ FilePreview,
+ type InputFile,
+ InputFileType,
+ MarkdownRenderer,
+ type MessageItem,
+ MessageRole,
+ type MessageStorage,
+ MessageType,
+ type MessageTypeDescriptor,
+ createChatStateAtoms,
+ getFileIcon,
+ getFileType,
+ handleFileInput,
+ handleImage,
+ normalizeFiles,
+ normalizeFilesPrompt,
+ parseCommandRules,
+ useChat,
+};
diff --git a/apps/agent-tars/src/vendor/chat-ui/index.js b/apps/agent-tars/src/vendor/chat-ui/index.js
new file mode 100644
index 000000000..616ad4bdd
--- /dev/null
+++ b/apps/agent-tars/src/vendor/chat-ui/index.js
@@ -0,0 +1 @@
+var t=Object.defineProperty,e=Object.defineProperties,n=Object.getOwnPropertyDescriptors,a=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,i=(e,n,a)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[n]=a,c=(t,e)=>{for(var n in e||(e={}))r.call(e,n)&&i(t,n,e[n]);if(a)for(var n of a(e))o.call(e,n)&&i(t,n,e[n]);return t},s=(t,a)=>e(t,n(a)),d=(t,e)=>{var n={};for(var i in t)r.call(t,i)&&e.indexOf(i)<0&&(n[i]=t[i]);if(null!=t&&a)for(var i of a(t))e.indexOf(i)<0&&o.call(t,i)&&(n[i]=t[i]);return n},l=(t,e,n)=>(i(t,"symbol"!==typeof e?e+"":e,n),n),u=(t,e,n)=>new Promise(((a,r)=>{var o=t=>{try{c(n.next(t))}catch(e){r(e)}},i=t=>{try{c(n.throw(t))}catch(e){r(e)}},c=t=>t.done?a(t.value):Promise.resolve(t.value).then(o,i);c((n=n.apply(t,e)).next())}));import p from"localforage";var w=class{constructor(t){l(this,"store"),l(this,"cache"),l(this,"id",0),this.store=p.createInstance({name:t}),this.cache=new Map}getMessageIds(t){return u(this,null,(function*(){const e=`messageIds-${t}`;return(yield this.store.getItem(e))||[]}))}getStore(){return this.store}setMessageIds(t,e){return u(this,null,(function*(){const n=`messageIds-${t}`;yield this.store.setItem(n,e)}))}getMessages(t){return u(this,null,(function*(){if(this.cache.has(t))return this.cache.get(t);const e=yield this.getMessageIds(t),n=(yield Promise.all(e.map((t=>this.store.getItem(t))))).filter(Boolean);return this.cache.set(t,n),n}))}uploadMessage(t){return u(this,null,(function*(){if(!t.conversationId)throw new Error("Message must have a conversationId to be uploaded");const e=`${t.conversationId}-${(new Date).getTime()}-${this.id++}`,n=s(c({},t),{id:e});yield this.store.setItem(e,n);const a=yield this.getMessageIds(t.conversationId);a.push(e),yield this.setMessageIds(t.conversationId,a);const r=this.cache.get(t.conversationId)||[];return r.push(n),this.cache.set(t.conversationId,r),n}))}updateMessage(t){return u(this,null,(function*(){if(!t.conversationId||!t.id)throw new Error("Message must have an id and conversationId to be updated");const e=yield this.store.getItem(t.id);if(e){const n=c(c({},e),t);return yield this.store.setItem(t.id,n),n}}))}clearMessages(t){return u(this,null,(function*(){const e=yield this.getMessageIds(t);for(const t of e)yield this.store.removeItem(t);yield this.setMessageIds(t,[]),this.cache.delete(t)}))}deleteMessage(t){return u(this,null,(function*(){yield this.store.removeItem(t);for(const[e,n]of Array.from(this.cache.entries())){const a=n.filter((e=>e.id!==t));if(a.length!==n.length){this.cache.set(e,a);const n=(yield this.getMessageIds(e)).filter((e=>e!==t));yield this.setMessageIds(e,n);break}}}))}};import{atom as g}from"jotai";function v(){return{messagesAtom:g([]),messageLoadingAtom:g(!1),imageLoadingAtom:g(!1),inputTextAtom:g(""),inputFilesAtom:g([]),uploadMessagePromiseRefAtom:g({current:null}),messageEndRefAtom:g({current:null}),messageSendingAtom:g(!1),isUserScrollingRefAtom:g({current:!1})}}var x=v();import{useAtom as b}from"jotai";import{useCallback as y}from"react";var h=(t=>(t.User="user",t.Assistant="assistant",t))(h||{}),f=(t=>(t.PlainText="plain-text",t.File="file",t))(f||{}),m=(t=>(t.Image="image",t.PDF="pdf",t.Text="text",t.Json="json",t.Zip="zip",t.Audio="audio",t.Video="video",t.Keynote="keynote",t.Powerpoint="powerpoint",t.Excel="excel",t.Word="word",t.PPTX="pptx",t.XLSX="xlsx",t.DOCX="docx",t.Other="other",t.JS="js",t.TS="ts",t.JSX="jsx",t.TSX="tsx",t.HTML="html",t.CSS="css",t.SCSS="scss",t.LESS="less",t.YAML="yaml",t.XML="xml",t.TOML="toml",t.Python="py",t.Java="java",t.Rust="rs",t.Swift="swift",t.Go="go",t.C="c",t.CPP="cpp",t.Stylus="stylus",t.PHP="php",t.Ruby="rb",t.Kotlin="kt",t.CSharp="cs",t.Draft="draft",t))(m||{}),k={"image":"#FFB6C1","pdf":"#FF6347","text":"#4682B4","json":"#32CD32","zip":"#FFD700","audio":"#8A2BE2","video":"#FF4500","keynote":"#00CED1","powerpoint":"#FF8C00","excel":"#228B22","word":"#1E90FF","pptx":"#FF7F50","xlsx":"#3CB371","docx":"#4169E1","js":"#FF6347","ts":"#FF6347","jsx":"#FF6347","tsx":"#FF6347","html":"#FF6347","css":"#FF6347","scss":"#FF6347","less":"#FF6347","yaml":"#FF6347","xml":"#FF6347","toml":"#FF6347","py":"#FF6347","rs":"#FF6347","go":"#FF6347","java":"#FF6347","swift":"#FF6347","c":"#FF6347","cpp":"#FF6347","other":"#D3D3D3","stylus":"#FF6347","php":"#FF6347","rb":"#FF6347","kt":"#FF6347","cs":"#FF6347","draft":"#FF6347"};function z({states:t=x,storageDbName:e,customMessageStorage:n=(e?new w(e):void 0),conversationId:a}={states:x,customMessageStorage:void 0,conversationId:void 0}){const{messagesAtom:r,messageLoadingAtom:o,imageLoadingAtom:i,inputTextAtom:l,inputFilesAtom:p,uploadMessagePromiseRefAtom:g,messageEndRefAtom:v,messageSendingAtom:h,isUserScrollingRefAtom:f}=t,[m,k]=b(o),[z,_]=b(i),[X,Y]=b(l),[S,j]=b(p),[C,F]=b(r),[M]=b(g),[I]=b(v),[T,A]=b(h),[N]=b(f),L=y(((t,e)=>{const r=(new Date).getTime(),o=s(c({},t),{timestamp:r,id:`${r}-${Math.random().toString(36).substring(2,15)}`});if(F((t=>[...t,o])),setTimeout((()=>{I.current&&I.current.scrollIntoView({behavior:"smooth",block:"nearest",inline:"nearest"})}),100),(null==e?void 0:e.shouldSyncStorage)&&n&&a){const t=()=>u(this,null,(function*(){const t=o,{id:e}=t,r=d(t,["id"]),i=yield n.uploadMessage(s(c({},r),{conversationId:a}));return i?(F((t=>t.map((t=>t.id===o.id?s(c({},t),{id:i.id}):t)))),M.current=null,i):null}));return M.current?M.current.then((()=>(M.current=t(),M.current))):(M.current=t(),M.current)}return Promise.resolve(o)}),[n,k,F,M,a,I.current]),R=y(((t,e)=>u(this,null,(function*(){var r;const o=null!=(r=null==e?void 0:e.shouldSyncStorage)?r:t.isFinal;let i;const d=new Promise((t=>i=t));F((r=>{let d=r.length-1;if((null==e?void 0:e.messageId)&&(d=r.findIndex((t=>t.id===e.messageId)),-1===d))return r;const l=c(c({},r[d]),t);if(o&&n){const t=()=>l.id?n.updateMessage(s(c({},l),{conversationId:a})):n.uploadMessage(s(c({},l),{conversationId:a}));if(M.current)M.current.then(t).then(i);else{const e=t();e&&"then"in e&&e.then(i)}}const u=[...r];return u[d]=l,u})),o&&(yield d),I.current&&!1!==(null==e?void 0:e.shouldScrollToBottom)&&!N.current&&I.current.scrollIntoView({behavior:"smooth",block:"nearest",inline:"nearest"})}))),[n,F,M,a]),D=y((()=>u(this,null,(function*(){if(!n||!a)return;k(!0);const t=Date.now(),e=yield n.getMessages(a);console.log(`initMessages: ${Date.now()-t}ms`);const r=e.map((t=>s(c({},t),{isFinal:!0}))).filter((t=>t.content));return F(r),k(!1),r}))),[a,n,k,F]),E=y((()=>u(this,null,(function*(){F((t=>t.map((t=>s(c({},t),{isDeleting:!0}))))),setTimeout((()=>{F([])}),250),n&&a&&(yield n.clearMessages(a)),k(!1)}))),[a,n,k,F]),P=y((t=>u(this,[t],(function*({deleteUIState:t=!0,deleteStorageRecord:e=!0}){if(0!==C.length&&(t&&F((t=>t.slice(0,-1))),e)){const t=C[C.length-1];t.id&&n&&(yield n.deleteMessage(t.id))}}))),[C,n,F]),$=y((t=>u(this,null,(function*(){const e=C.find((e=>e.id===t));e&&(e.isDeleting=!0,F([...C]),setTimeout((()=>{F((e=>e.filter((e=>e.id!==t))))}),250),n&&e.id&&(yield n.deleteMessage(e.id)))}))),[C,n,F]);return{addMessage:L,updateMessage:R,messages:C,setMessages:F,initMessages:D,clearMessages:E,deleteLastMessage:P,deleteMessage:$,messageLoading:m,setMessageLoading:k,imageLoading:z,setImageLoading:_,inputText:X,setInputText:Y,inputFiles:S,setInputFiles:j,messageSending:T,setMessageSending:A,messageEndRef:I}}import{useRef as _,useState as X}from"react";import{Tooltip as Y}from"@nextui-org/react";import{jsx as S}from"react/jsx-runtime";function j(t){return S("svg",s(c({xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24"},t),{children:S("path",{fill:"currentColor",d:"M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12z"})}))}import{jsx as C}from"react/jsx-runtime";var F=t=>C("svg",s(c({width:"32",height:"32",viewBox:"0 0 30 30"},t),{children:C("path",{fill:"#49cd37",d:"m13 24l-9-9l1.414-1.414L13 21.171L26.586 7.586L28 9L13 24z"})}));var M=function(t,e){void 0===e&&(e={});var n=e.insertAt;if(t&&"undefined"!==typeof document){var a=document.head||document.getElementsByTagName("head")[0],r=document.createElement("style");r.type="text/css","top"===n&&a.firstChild?a.insertBefore(r,a.firstChild):a.appendChild(r),r.styleSheet?r.styleSheet.cssText=t:r.appendChild(document.createTextNode(t))}};M(".ai-app-message-copy-button {\n position: relative;\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 10;\n border-radius: var(--chat-ui-copy-button-border-radius);\n padding: 0.3rem;\n border: 1px solid var(--chat-ui-copy-button-border-color);\n background-color: var(--chat-ui-copy-button-background-color);\n color: var(--chat-ui-copy-button-text-color);\n}\n.ai-app-message-copy-button svg {\n opacity: inherit;\n width: 16px;\n height: 16px;\n transition: all 0.2s ease;\n color: var(--chat-ui-copy-button-text-color);\n}\n.ai-app-message-copy-button svg:hover {\n color: var(--chat-ui-copy-button-text-color);\n}\n.ai-app-message-copy-button .ai-app-message-icon-success {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%) scale(0);\n opacity: 0;\n color: #00d600;\n}\n\n.ai-app-message-copied .ai-app-message-icon-copy {\n transform: scale(0.33);\n opacity: 0;\n}\n.ai-app-message-copied .ai-app-message-icon-success {\n transform: translate(-50%, -50%) scale(1);\n opacity: 1;\n transition-delay: 0.075s;\n}");import{jsx as I,jsxs as T}from"react/jsx-runtime";var A=new Map,N=({getCopyText:t,tooltipText:e="点击复制",successText:n="复制成功!"})=>{const a=_(null),[r,o]=X(!1);return I(Y,{content:r?n:e,children:T("button",{ref:a,className:"ai-app-message-copy-button",onClick:()=>{navigator.clipboard.writeText(t()).then((()=>{const t=a.current,e="ai-app-message-copied";if(t){t.classList.add(e),clearTimeout(A.get(t)),o(!0);const n=setTimeout((()=>{o(!1),t.classList.remove(e),t.blur(),A.delete(t)}),2e3);A.set(t,n)}}))},children:[I(j,{className:"ai-app-message-icon-copy"}),I(F,{className:"ai-app-message-icon-success"})]})})};import{AiOutlineFileExcel as L,AiOutlineFilePpt as R,AiFillAudio as D,AiFillVideoCamera as E,AiFillFileText as P}from"react-icons/ai";import{VscJson as $,VscFilePdf as O,VscFileZip as B,VscFileMedia as W,VscFileCode as H}from"react-icons/vsc";import{FaJs as U,FaPython as q,FaJava as V,FaSass as J,FaReact as K,FaHtml5 as G,FaCss3Alt as Z,FaPhp as Q}from"react-icons/fa";import{SiTypescript as tt,SiRust as et,SiSwift as nt,SiGo as at,SiCplusplus as rt,SiLess as ot,SiStylus as it,SiRuby as ct,SiKotlin as st,SiCsharp as dt}from"react-icons/si";import{jsx as lt}from"react/jsx-runtime";var ut=t=>{const e=(t=>{let e="";const n=new Uint8Array(t),a=n.byteLength;for(let r=0;r{var e;const n=null==(e=t.split(".").pop())?void 0:e.toLowerCase();switch(n){case"pdf":return"pdf";case"txt":return"text";case"json":return"json";case"zip":return"zip";case"mp3":case"wav":return"audio";case"mp4":case"avi":return"video";case"key":return"keynote";case"ppt":return"powerpoint";case"xls":return"excel";case"doc":return"word";case"pptx":return"pptx";case"xlsx":return"xlsx";case"docx":return"docx";case"stylus":return"stylus";case"php":return"php";case"rb":return"rb";case"kt":return"kt";case"cs":return"cs";default:return n}},wt={"pdf":t=>lt(O,{size:t}),"text":t=>lt(P,{size:t}),"zip":t=>lt(B,{size:t}),"audio":t=>lt(D,{size:t}),"video":t=>lt(E,{size:t}),"docx":t=>lt(P,{size:t}),"pptx":t=>lt(R,{size:t}),"powerpoint":t=>lt(R,{size:t}),"excel":t=>lt(L,{size:t}),"xlsx":t=>lt(L,{size:t}),"keynote":t=>lt(P,{size:t}),"json":t=>lt($,{size:t}),"word":t=>lt(P,{size:t}),"image":t=>lt(W,{size:t}),"yaml":t=>lt(H,{size:t}),"xml":t=>lt(H,{size:t}),"toml":t=>lt(H,{size:t}),"js":t=>lt(U,{size:t,color:"#F7DF1E"}),"ts":t=>lt(tt,{size:t,color:"#3178C6"}),"jsx":t=>lt(K,{size:t,color:"#61DAFB"}),"tsx":t=>lt(K,{size:t,color:"#61DAFB"}),"html":t=>lt(G,{size:t,color:"#E34F26"}),"css":t=>lt(Z,{size:t,color:"#1572B6"}),"scss":t=>lt(J,{size:t,color:"#CC6699"}),"less":t=>lt(ot,{size:t,color:"#1D365D"}),"py":t=>lt(q,{size:t,color:"#3776AB"}),"java":t=>lt(V,{size:t,color:"#007396"}),"rs":t=>lt(et,{size:t,color:"#B7410E"}),"swift":t=>lt(nt,{size:t,color:"#FA7343"}),"go":t=>lt(at,{size:t,color:"#00ADD8"}),"c":t=>lt(rt,{size:t,color:"#00599C"}),"cpp":t=>lt(rt,{size:t,color:"#00599C"}),"stylus":t=>lt(it,{size:t,color:"#FF6347"}),"php":t=>lt(Q,{size:t,color:"#777BB4"}),"rb":t=>lt(ct,{size:t,color:"#CC342D"}),"kt":t=>lt(st,{size:t,color:"#0095D5"}),"draft":t=>lt(H,{size:t}),"cs":t=>lt(dt,{size:t,color:"#239120"}),"other":t=>lt(H,{size:t})};function gt(t,e=16){const n=pt(t);return(wt[n]||wt.other)(e)}var vt={};function xt(t){return u(this,null,(function*(){const e=t.filter((t=>"image"===t.type)),n=e.map((({content:t})=>t)).map((t=>({role:"user",content:[{type:"image_url",image_url:{url:t}}]})));n.length>0&&n.push({role:"user",content:`The uploaded image links: [${e.map((t=>t.content)).join(", ")}]`});const a=t.filter((t=>"image"!==t.type)).map((t=>({role:"user",content:`This is a ${t.type} file, the content is: \n${t.content}`})));return[...n,...a]}))}function bt(t){return u(this,null,(function*(){const e=t.filter((t=>"image"===t.type)),n=yield function(t){return u(this,null,(function*(){return yield Promise.all(t.map((t=>u(this,null,(function*(){if(vt[t])return vt[t];const e=yield fetch(t),n=yield e.blob();return new Promise(((e,a)=>{const r=new FileReader;r.onload=()=>{const n=r.result;vt[t]=n,e(n)},r.onerror=a,r.readAsDataURL(n)}))})))))}))}(e.map((({content:t})=>t)));return t.map((t=>{if("image"===t.type){const a=e.findIndex((e=>e.content===t.content));return s(c({},t),{content:n[a]})}return t}))}))}function yt(t){return t.reduce(((t,e)=>(e.label&&t.push(e.label),e.items&&e.items.length>0&&t.push(...yt(e.items)),t)),[])}import ht from"jszip";var ft=t=>u(void 0,null,(function*(){const e=yield(n="https://esm.sh/v44/pdfjs-dist@4.4.168/es2021/pdfjs-dist.js",new Function(`return import('${n}')`)());var n;e.GlobalWorkerOptions.workerSrc="https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.min.mjs";const a=yield e.getDocument({data:t}).promise;let r="";for(let t=1;t<=a.numPages;t++){const e=yield a.getPage(t);(yield e.getTextContent()).items.forEach((t=>{"str"in t&&(r+=`${t.str} `)}))}return r.trim().slice(0,1e4)})),mt=(t,e,n,a=!1)=>{t((t=>t.map(((t,r)=>r===e?s(c({},t),{content:n,loading:a}):t))))},kt=(t,e)=>u(void 0,null,(function*(){let n;e((e=>(n=e.length,[...e,{type:"image",content:"",loading:!0,originalFile:t}])));try{const a=new FileReader,r=yield new Promise(((e,n)=>{a.onload=()=>e(a.result),a.onerror=n,a.readAsDataURL(t)}));if("undefined"===typeof n)return;mt(e,n,r,!1)}catch(a){console.error("文件转换失败",a),e((t=>t.filter(((t,e)=>e!==n))))}})),zt=(t,e)=>u(void 0,null,(function*(){const n=pt(t.name),a=t.size/1024;if(t.type.startsWith("image/"))yield kt(t,e);else{let r;e((e=>(r=e.length,[...e,{type:n,filename:t.name,content:"",size:a,loading:!0,originalFile:t}])));const o=new FileReader;o.onload=()=>u(void 0,null,(function*(){let t;var a;t=o.result instanceof ArrayBuffer?"pdf"===n?yield ft(o.result):"zip"===n?yield(a=o.result,u(void 0,null,(function*(){const t=yield ht.loadAsync(a);let e="";for(const[n,a]of Object.entries(t.files))a.dir||(e+=`\n\n--- ${n} ---\n\n${yield a.async("string")}`);return e.trim()}))):ut(o.result):o.result,mt(e,r,t,!1)})),"pdf"===n||"audio"===n||"video"===n||"zip"===n?o.readAsArrayBuffer(t):o.readAsText(t)}}));import{useState as _t}from"react";M('@charset "UTF-8";\n.filePreviewContainer_28e0b {\n display: flex;\n gap: 8px;\n margin-right: 8px;\n width: 100%;\n overflow-x: auto;\n}\n\n.filePreviewWrapper_28e0b {\n display: flex;\n position: relative;\n}\n\n.imagePreview_28e0b {\n display: flex;\n max-width: 400px;\n width: 100%;\n height: 100%;\n border: 1px solid #ccc;\n border-radius: 8px;\n position: relative;\n cursor: pointer;\n transition: background-color 0.3s ease;\n object-fit: cover;\n}\n\n.imagePreview_28e0b.visible_28e0b {\n opacity: 1;\n}\n\n.imagePreview_28e0b:hover .deleteIcon_28e0b {\n display: flex; /* 显示删除图标 */\n}\n\n.deleteIcon_28e0b {\n position: absolute;\n z-index: 10;\n top: -8px;\n right: -5px;\n border-radius: 50%;\n cursor: pointer;\n font-size: 18px;\n line-height: 12px;\n width: 24px;\n height: 24px;\n}\n\n.fullscreenOverlay_28e0b {\n z-index: 1000;\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.fullscreenImage_28e0b {\n max-width: 90%;\n max-height: 90%;\n animation: fadeIn_28e0b 0.3s ease;\n}\n\n@keyframes fadeIn_28e0b {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}');var Xt="filePreviewContainer_28e0b",Yt="filePreviewWrapper_28e0b",St="imagePreview_28e0b",jt="deleteIcon_28e0b",Ct="fullscreenOverlay_28e0b",Ft="fullscreenImage_28e0b";import{Spinner as Mt,Tooltip as It}from"@nextui-org/react";import{jsx as Tt,jsxs as At}from"react/jsx-runtime";var Nt=t=>At("svg",s(c({viewBox:"64 64 896 896",focusable:"false"},t),{children:[Tt("path",{d:"M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64z",fill:"#f9f9f9"}),Tt("path",{d:"M639.98 338.88h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z",fill:"#000000"})]}));import{Fragment as Lt,jsx as Rt,jsxs as Dt}from"react/jsx-runtime";var Et=({file:t})=>{const e=void 0===t.size?"":t.size<1024?`${t.size.toFixed(2)} KB`:`${(t.size/1024).toFixed(2)} MB`;return Dt("div",{className:"flex gap-2 p-2 pr-4",style:{border:"1px solid #ccc",borderRadius:"10px"},children:[Rt("div",{className:"flex justify-center items-center",style:{width:"44px",height:"100%",borderRadius:6,backgroundColor:k[t.type],color:"var(--chat-ui-text-color)",fontSize:24},children:gt(t.filename||"")}),Dt("div",{className:"flex flex-col text-sm gap-1",children:[Rt(It,{content:t.filename,children:Rt("div",{style:{maxWidth:"200px",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",fontWeight:600},children:t.filename})}),Dt("div",{className:"flex gap-2 text-sm",children:[Rt("div",{className:"text-gray-400 font-light",children:t.type}),e?Rt("div",{className:"text-gray-400 font-light",children:e}):null]})]})]})};function Pt({file:t,handleDelete:e,showDeleteIcon:n=!0,itemStyle:a}){const[r,o]=_t(!1),[i,c]=_t(!1),s=()=>{"image"===t.type&&(c(!0),document.body.style.overflow="hidden")};return Dt(Lt,{children:[Dt("div",{className:Yt,onMouseEnter:()=>o(!0),onMouseLeave:()=>o(!1),style:a,children:[!0===t.loading?Rt("div",{style:{width:"56px",height:"56px"},className:"flex justify-center items-center",children:Rt(Mt,{size:"sm"})}):"image"===t.type?Rt("img",{src:t.content,className:St,alt:"pasted",style:a,onClick:s}):Rt(Et,{file:t}),n?Rt("button",{onClick:e,className:jt,style:{display:r?"block":"none"},children:Rt("div",{style:{border:"1px solid #ccc",borderRadius:"50%"},children:Rt(Nt,{})})}):null]}),i?Rt("div",{className:Ct,onClick:()=>{c(!1),document.body.style.overflow="auto"},children:Rt("img",{src:t.content,alt:"fullscreen",className:Ft})}):null]})}function $t({files:t,handleDelete:e,showDeleteIcon:n=!0,containerStyle:a={},itemStyle:r={}}){const o={width:"56px",height:"56px"};return Rt("div",{className:`${Xt}`,style:c({marginTop:t.length>0?"-8px":"0",marginBottom:t.length>0?"8px":"0",paddingTop:t.length>0?"8px":"0"},a),children:t.map(((t,a)=>Rt(Pt,{file:t,handleDelete:()=>null==e?void 0:e(a),showDeleteIcon:n,itemStyle:c(c({},"image"===t.type?o:{}),r)},a)))})}function Ot(t,e){for(const n of t){if(n.label===e)return n;if(n.items){const t=Ot(n.items,e);if(t)return t}}}function Bt(t,e){const n=[],a=Object.keys(e).join(""),r=new RegExp(`([${a}][\\w\\u4e00-\\u9fa5:\\/\\.\\-]+)(\\s|$)`,"g"),o=t.split(r);for(let i=0;it.startsWith(e)))){const a=t[0],r=t.slice(1);if(!r)continue;const o=Ot(e[a].data,r);o?n.push({type:"commandText",content:o.name}):n.push({type:"text",content:t.trim()}),i++}else t.trim()&&n.push({type:"text",content:t.trim()})}return n}import{useState as Wt,useEffect as Ht,useCallback as Ut,useRef as qt,useMemo as Vt}from"react";import{Card as Jt,Spinner as Kt}from"@nextui-org/react";import{motion as Gt,AnimatePresence as Zt}from"framer-motion";import{FiArrowLeft as Qt,FiArrowRight as te}from"react-icons/fi";M(".ai-app-at-panel {\n position: absolute;\n left: 0;\n right: 0;\n margin: 0.5rem 0;\n max-width: 100%;\n z-index: 1000;\n}\n.ai-app-at-panel.above {\n bottom: 100%;\n margin-bottom: 0.5rem;\n}\n.ai-app-at-panel.above .ai-app-at-panel-card {\n margin-bottom: 0.5rem;\n}\n.ai-app-at-panel.below {\n width: 90%;\n margin: auto;\n top: 30%;\n margin-top: 0.5rem;\n}\n.ai-app-at-panel.below .ai-app-at-panel-card {\n margin-top: 0.5rem;\n}\n.ai-app-at-panel-no-results {\n padding: 4px 16px;\n color: var(--nextui-colors-accents6);\n font-size: 14px;\n text-align: center;\n}\n\n.ai-app-at-panel-card {\n padding: var(--chat-ui-at-panel-padding);\n background-color: var(--chat-ui-at-panel-bg);\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n border-radius: var(--chat-ui-at-panel-border-radius);\n border: 1px solid var(--chat-ui-at-panel-border);\n max-height: 400px;\n overflow: auto !important;\n overflow-x: hidden;\n}\n.ai-app-at-panel-card::-webkit-scrollbar {\n width: 6px;\n height: 6px;\n}\n.ai-app-at-panel-card::-webkit-scrollbar-track {\n background: var(--chat-ui-at-panel-scrollbar-track);\n}\n.ai-app-at-panel-card::-webkit-scrollbar-thumb {\n background: var(--chat-ui-at-panel-scrollbar-thumb);\n border-radius: 3px;\n}\n.ai-app-at-panel-card::-webkit-scrollbar-thumb:hover {\n background: var(--chat-ui-at-panel-scrollbar-thumb-hover);\n}\n\n.ai-app-at-panel-item {\n display: flex;\n align-items: center;\n gap: var(--chat-ui-at-panel-item-gap);\n justify-content: space-between;\n padding: var(--chat-ui-at-panel-item-padding);\n border-radius: var(--chat-ui-at-panel-item-border-radius);\n font-size: 0.75rem;\n cursor: pointer;\n transition: background-color 0.2s;\n color: var(--chat-ui-at-panel-text);\n width: fit-content;\n min-width: 100%;\n}\n\n.ai-app-at-panel-item:hover {\n background-color: var(--chat-ui-at-panel-item-hover);\n}\n\n.ai-app-at-panel-item-selected {\n background-color: var(--chat-ui-at-panel-item-selected);\n}\n\n.ai-app-at-panel-item-content {\n display: flex;\n align-items: center;\n}\n\n.ai-app-at-panel-item-name {\n margin-left: 0.75rem;\n font-size: 0.75rem;\n color: var(--chat-ui-at-panel-text-dark);\n}\n\n.ai-app-at-panel-item-label {\n flex-shrink: 0;\n}\n\n.ai-app-at-panel-item-description {\n font-size: 0.675rem;\n color: var(--chat-ui-at-panel-text-light);\n margin-left: 0.5rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 400px;\n direction: rtl;\n text-align: left;\n}");import{jsx as ee,jsxs as ne}from"react/jsx-runtime";function ae({config:t,isShow:e,setIsShow:n,inputRef:a,filterText:r,setInputText:o,setFilterText:i,command:c,onSelect:s,isLoading:d}){const[l,u]=Wt(0),[p,w]=Wt({items:t.data,label:"",name:"",description:""}),[g,v]=Wt([p]),x=1===g.length,b=qt(null),y=qt(!1),[h,f]=Wt(!0);Ht((()=>{if(e&&a.current&&b.current){const t=a.current.getBoundingClientRect(),e=b.current.offsetHeight,n=window.innerHeight,r=t.top,o=n-t.bottom;f(!(r=e))}}),[e,r,a]);const m=Vt((()=>{var t,e;const n=g[g.length-1];if(!r)return null==(t=n.items)?void 0:t.slice(0,30);const a=null==(e=n.items)?void 0:e.flatMap((t=>t.items||t));return r&&a?a.filter((t=>{var e;return t.label.toLowerCase().includes(r.toLowerCase())||(null==(e=t.description)?void 0:e.toLowerCase().includes(r.toLowerCase()))})).slice(0,30):null==a?void 0:a.slice(0,30)}),[g,r]);Ht((()=>{u(0)}),[r]);const k=Ut((t=>{if(t.items&&t.items.length>0)v((e=>[...e,t])),w(t),u(0);else{const e=a.current;if(e){const n=e.selectionStart,a=e.value.substring(0,n),r=e.value.substring(n),s=a.lastIndexOf(c),d=`${a.substring(0,s)}${c}${t.label} ${r}`;o(d),i(""),setTimeout((()=>{e.setSelectionRange(s+t.label.length+2,s+t.label.length+2),e.focus()}),0)}n(!1),null==s||s(c,t)}}),[a,n,s,c,o,i]),z=Ut((()=>{if(g.length>1){const t=g[g.length-2];v((t=>t.slice(0,-1))),w(t),u(0)}else n(!1)}),[g,n]);return Ht((()=>{const r=r=>{if(y.current&&"Enter"===r.key)y.current=!1;else if(e&&!y.current)if("ArrowUp"===r.key||"ArrowDown"===r.key)u((t=>{const e=(null==m?void 0:m.length)||0,n=x?e:e+1;if(0===n)return t;let a;return a="ArrowUp"===r.key?(t-1+n)%n:(t+1)%n,setTimeout((()=>{var t;const e=null==(t=b.current)?void 0:t.querySelector(".ai-app-at-panel-item-selected");null==e||e.scrollIntoView({behavior:"smooth",block:"nearest"})}),0),a})),r.preventDefault(),r.stopPropagation();else if("Enter"===r.key)m&&m.length>0&&(l!==m.length||x?l{t.focus()}),0);const n=t.selectionStart,r=t.value.substring(0,n),s=t.value.substring(n),d=r.lastIndexOf(c),u=`${r.substring(0,d)}${c}${e.selectText}${s}`;o(u),i(e.selectText),setTimeout((()=>{t.setSelectionRange(u.length,u.length),t.focus()}),0)}},s=()=>{y.current=!0},d=t=>{y.current=!1,i((e=>e+t.data))},p=a.current;return p&&(p.addEventListener("compositionstart",s),p.addEventListener("compositionend",d)),document.addEventListener("keydown",r),()=>{document.removeEventListener("keydown",r),p&&(p.removeEventListener("compositionstart",s),p.removeEventListener("compositionend",d))}}),[e,l,m,k,z,a,o,n,x,r,t,i,c]),ee(Zt,{children:e?ee(Gt.div,{initial:{opacity:0,y:h?10:-10},animate:{opacity:1,y:0},exit:{opacity:0,y:h?10:-10},transition:{duration:.2},className:"ai-app-at-panel "+(h?"above":"below"),ref:b,children:ne(Jt,{className:"ai-app-at-panel-card",style:{borderRadius:"var(--chat-ui-at-panel-border-radius)"},children:[d?ee("div",{className:"flex justify-center items-center p-4",children:ee(Kt,{size:"sm"})}):m&&m.length>0?m.map(((t,e)=>{var n;return ne(Gt.div,{className:"ai-app-at-panel-item "+(e===l?"ai-app-at-panel-item-selected":""),onClick:()=>k(t),children:[ne("div",{className:"ai-app-at-panel-item-content flex items-center gap-2",children:[t.icon,ee("span",{className:"ai-app-at-panel-item-label",children:null!=(n=t.selectText)?n:t.label}),t.description?ee("span",{className:"ai-app-at-panel-item-description",children:t.description}):null]}),t.items&&t.items.length>0?ee(te,{size:16}):null]},t.name)})):ee("div",{className:"ai-app-at-panel-no-results",children:"无匹配结果(按 Esc 可取消面板)"}),d||x||!p.label?null:ee(Gt.div,{className:"ai-app-at-panel-item "+(l===(null==m?void 0:m.length)?"ai-app-at-panel-item-selected":""),onClick:z,children:ne("div",{className:"ai-app-at-panel-item-content",children:[ee(Qt,{size:16}),ne("span",{className:"ai-app-at-panel-item-name",children:["返回: ",p.label]})]})})]})}):null})}import{jsx as re,jsxs as oe}from"react/jsx-runtime";function ie({size:t=20,className:e}){return oe("svg",{width:t,height:t,viewBox:"0 0 24 24",className:e,xmlns:"http://www.w3.org/2000/svg",children:[re("path",{d:"M12 0h4c2.208 0 4 1.792 4 4s-1.792 4-4 4h-4V0z",fill:"#FF4D3D"}),re("path",{d:"M4 4c0-2.208 1.792-4 4-4h4v8H8C5.792 8 4 6.208 4 4z",fill:"#9747FF"}),re("path",{d:"M4 12c0-2.208 1.792-4 4-4h4v8H8c-2.208 0-4-1.792-4-4z",fill:"#18A0FB"}),re("path",{d:"M8 24c2.208 0 4-1.792 4-4v-4H8c-2.208 0-4 1.792-4 4s1.792 4 4 4z",fill:"#09CF83"}),re("path",{d:"M20 12c0 2.208-1.792 4-4 4s-4-1.792-4-4 1.792-4 4-4 4 1.792 4 4z",fill:"#FF4D3D"})]})}import{Tooltip as ce}from"@nextui-org/react";import{useRef as se,useCallback as de,memo as le,useState as ue,useEffect as pe,forwardRef as we,useImperativeHandle as ge}from"react";import{jsx as ve}from"react/jsx-runtime";function xe(t){return ve("svg",s(c({xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24"},t),{children:ve("path",{fill:"currentColor",d:"M19 4h-3.5l-1-1h-5l-1 1H5v2h14M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6z"})}))}import{jsx as be}from"react/jsx-runtime";function ye(t){return be("svg",s(c({xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24"},t),{children:be("path",{fill:"currentColor",d:"M16.5 6v11.5a4 4 0 0 1-4 4a4 4 0 0 1-4-4V5A2.5 2.5 0 0 1 11 2.5A2.5 2.5 0 0 1 13.5 5v10.5a1 1 0 0 1-1 1a1 1 0 0 1-1-1V6H10v9.5a2.5 2.5 0 0 0 2.5 2.5a2.5 2.5 0 0 0 2.5-2.5V5a4 4 0 0 0-4-4a4 4 0 0 0-4 4v12.5a5.5 5.5 0 0 0 5.5 5.5a5.5 5.5 0 0 0 5.5-5.5V6z"})}))}M(".ai-app-input-section-container {\n position: relative;\n padding: 1px;\n background: var(--chat-ui-input-textarea-bg);\n background-size: 300% 300%;\n animation: shineAnimation 6s linear infinite;\n border-radius: 8px;\n margin: 0;\n width: 100%;\n}\n\n.ai-app-input-section-wrapper {\n position: relative;\n border-radius: 7px;\n padding: 16px;\n display: flex;\n flex-direction: column;\n}\n\n.ai-app-input-textarea {\n width: 100%;\n min-height: 40px;\n border: none;\n font-size: 14px;\n background: transparent;\n color: var(--chat-ui-input-textarea-color);\n outline: none;\n resize: none;\n padding: 0;\n margin: 0;\n overflow: auto;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.ai-app-input-textarea::placeholder {\n font-size: 14px;\n color: var(--chat-ui-input-textarea-placeholder-color);\n}\n.ai-app-input-textarea:focus {\n outline: none;\n}\n\n.ai-app-input-section-button-container {\n display: flex;\n justify-content: space-between;\n align-items: center;\n position: relative;\n}\n\n.ai-app-input-section-button-left {\n display: flex;\n gap: 4px;\n}\n\n.ai-app-input-section-menu-button {\n padding: 8px;\n background-color: transparent;\n border: none;\n border-radius: 10px;\n cursor: pointer;\n transition: all 0.3s ease;\n font-size: 16px;\n color: #555;\n}\n.ai-app-input-section-menu-button:hover {\n background-color: var(--chat-ui-input-section-menu-button-hover-bg);\n transform: scale(1.1);\n}\n\n.ai-app-send-button {\n padding: 8px 8px;\n background-color: #016fee;\n color: #ffffff;\n border: none;\n cursor: pointer;\n transition: all 0.3s ease;\n opacity: 0;\n transform: scale(0.8);\n animation: fadeInScale 0.3s forwards;\n border-radius: 12px;\n font-weight: 600;\n box-shadow: 0 2px 10px rgba(52, 152, 219, 0.3);\n}\n.ai-app-send-button:hover {\n transform: translateY(-2px) scale(1.05);\n background-color: #2980b9;\n box-shadow: 0 4px 15px rgba(52, 152, 219, 0.5);\n}\n.ai-app-send-button.stop {\n background-color: #ff6347;\n box-shadow: 0 2px 10px rgba(255, 99, 71, 0.3);\n}\n.ai-app-send-button.stop:hover {\n transform: translateY(-2px) scale(1.05);\n background-color: #e05734;\n box-shadow: 0 4px 15px rgba(255, 99, 71, 0.5);\n}\n\n@keyframes shineAnimation {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n@keyframes fadeInScale {\n to {\n opacity: 1;\n transform: scale(1);\n }\n}");import{useAtom as he}from"jotai";import{IoArrowUp as fe,IoStop as me}from"react-icons/io5";import ke from"classnames";import{jsx as ze,jsxs as _e}from"react/jsx-runtime";var Xe=le(we((({disableInput:t,userMessages:e,sendMessage:n,abortMessage:a,clearMessages:r,slots:o,style:i={},features:c={},chatStates:s,commandPanelConfig:d,inputPlaceholder:l,onCommandTrigger:p,onCommandSelect:w,onCommandDelete:g,onClearConversationHistory:v},x)=>{const{inputTextAtom:b,inputFilesAtom:y,messageEndRefAtom:h,imageLoadingAtom:f,messageLoadingAtom:m,messageSendingAtom:k}=s,[z,_]=he(b),[X,Y]=he(y),[S]=he(h),[j]=he(m),[C]=he(f),[F,M]=ue(""),I=se(null),T=se(null),{clearConversationHistory:A,uploadFiles:N}=c,[L,R]=ue(!1),[D,E]=he(k),[P,$]=ue(e.length),[O,B]=ue(!1);pe((()=>{$(e.length)}),[e.length]);const W=de((t=>{Y((e=>{const n=[...e];return n.splice(t,1),n}))}),[Y]),H=de((t=>u(void 0,null,(function*(){if(document.activeElement!==I.current)return;const{files:e}=t.clipboardData;if(e.length>0){const n=e[0];t.preventDefault(),yield zt(n,Y)}}))),[Y]),U=de((()=>{null==v||v(),null==r||r()}),[r,v]),[q,V]=ue(!1),[J,K]=ue(null),G=de(((t,e)=>u(void 0,null,(function*(){if(!(!t.trim()&&!e.length||j||C||q)){E(!0),_(""),Y([]),setTimeout((()=>{var t;null==(t=S.current)||t.scrollIntoView({behavior:"smooth",block:"nearest",inline:"nearest"})}));try{yield null==n?void 0:n(t,e)}finally{E(!1)}}}))),[j,C,q,E,_,Y,S,n]),Z=de((()=>u(void 0,null,(function*(){D?(null==a||a(),E(!1)):yield G(z,X)}))),[D,a,E,G,z,X]),Q=de((t=>u(void 0,null,(function*(){var n,a,r;if(document.activeElement===I.current&&!L&&229!==t.keyCode)if(q&&"Escape"===t.key)V(!1);else{if("ArrowLeft"===t.key||"ArrowRight"===t.key){const e=(null==(n=I.current)?void 0:n.selectionStart)||0,o=(null==(a=I.current)?void 0:a.value)||"";let i=e;const c=yt(Object.values(d||{}).flatMap((t=>t.data)));if("ArrowLeft"===t.key)for(const n of Object.keys(d||{})){const a=c.find((t=>o.substring(0,e).endsWith(`${n}${t}`)));if(a){const r=n.length+a.length;i=e-r,t.preventDefault();break}if(o.substring(0,e).endsWith(n)){i=e-n.length,t.preventDefault();break}}else for(const n of Object.keys(d||{})){const a=c.find((t=>o.substring(e).startsWith(`${n}${t}`)));if(a){const r=n.length+a.length;i=e+r,t.preventDefault();break}if(o.substring(e).startsWith(n)){i=e+n.length,t.preventDefault();break}}i!==e&&(null==(r=I.current)||r.setSelectionRange(i,i))}if("ArrowUp"!==t.key)if("ArrowDown"!==t.key){if("Enter"===t.key){if(t.shiftKey)return;if(!q&&!t.metaKey&&!t.ctrlKey){if(D)return;return t.preventDefault(),t.stopPropagation(),E(!0),yield G(z,X),void E(!1)}}if("Backspace"===t.key&&d&&I.current){const e=I.current.selectionStart,n=I.current.value.substring(0,e),a=I.current.selectionStart,r=I.current.selectionEnd,o=yt(Object.values(d).flatMap((t=>t.data)));for(const c of Object.keys(d))if(n.endsWith(c)&&a===r){t.preventDefault();const n=z.slice(0,e-c.length)+z.slice(e);return _(n),setTimeout((()=>{var t;null==(t=I.current)||t.setSelectionRange(e-c.length,e-c.length)}),0),V(!1),void K(null)}const i=o.find((t=>Object.keys(d).some((e=>n.endsWith(`${e}${t}`)))));if(i){t.preventDefault();const a=Object.keys(d).find((t=>n.endsWith(`${t}${i}`)));if(a){const t=a.length+i.length,n=z.slice(0,e-t)+z.slice(e);_(n),setTimeout((()=>{var n;null==(n=I.current)||n.setSelectionRange(e-t,e-t)}),0);const r=Ot(d[a].data,i);r&&(null==g||g(a,r))}V(!1),K(null)}}}else{const t=I.current;if(!t)return;if(P===e.length-1)return;if(t.selectionStart===t.value.length){const n=e[P+1].content;$((t=>t+1)),_(n),setTimeout((()=>{t.setSelectionRange(n.length,n.length),t.focus()}),0)}}else{const t=I.current;if(!t)return;if(0===P)return;if(0===t.selectionStart){const n=e[P-1].content;$((t=>t-1)),_(n),setTimeout((()=>{t.setSelectionRange(0,0),t.focus()}),0)}}}}))),[L,q,d,P,e,_,D,E,G,z,X,g]);pe((()=>(window.addEventListener("keydown",Q),()=>{window.removeEventListener("keydown",Q)})),[Q]);const tt=de((t=>u(void 0,null,(function*(){const{value:e}=t.target;if(_(e),L)return;const n=t.target.selectionStart,a=e.substring(0,n||0);for(const t of Object.keys(d||{})){const e=a.lastIndexOf(t);if(-1!==e){const n=a.slice(e+t.length);if(!/\s/.test(n))return p&&(B(!0),yield p(t),B(!1)),V(!0),K(t),void M(n)}}V(!1),K(null),M("")}))),[_,L,d,p]),et=de((()=>{R(!0)}),[]),nt=de((()=>{R(!1)}),[]),at=de((t=>u(void 0,null,(function*(){if((null==d?void 0:d[t])&&(p&&(B(!0),yield p(t),B(!1)),V(!0),K(t),M(""),I.current)){const e=I.current.selectionStart||0,n=z.slice(0,e)+t+z.slice(e);_(n),setTimeout((()=>{if(I.current){const n=e+t.length;I.current.setSelectionRange(n,n),I.current.focus()}}),0)}}))),[d,z,_,p]);return ge(x,(()=>({triggerCommand:at,focus:()=>{var t;null==(t=I.current)||t.focus()},getInputTextArea:()=>I.current}))),ze("div",{className:"ai-app-input-section-container",children:_e("div",{className:"ai-app-input-section-wrapper",children:[_e("div",{className:"flex-1",children:[o.beforeInputArea,ze("textarea",{className:"ai-app-input-textarea scrollbar","aria-label":"Prompt",rows:3,ref:I,style:i,disabled:t,placeholder:t?"暂时无法对话":l,onChange:t=>tt(t),onPaste:t=>H(t),value:z,onCompositionStart:et,onCompositionEnd:nt}),o.afterInputArea,d&&q&&J?ze(ae,{config:d[J],isShow:q,setIsShow:V,inputRef:I,filterText:F,setFilterText:M,setInputText:_,onSelect:w,command:J,isLoading:O}):null]}),ze("div",{className:"w-full",children:ze($t,{files:X,handleDelete:W})}),o.secondaryFeatures?ze("div",{className:"ai-app-input-section-button-container",children:o.secondaryFeatures}):null,_e("div",{className:"ai-app-input-section-button-container",children:[_e("div",{className:"ai-app-input-section-button-left",children:[N?ze(ce,{content:"Upload File",children:_e("button",{className:"ai-app-input-section-menu-button",disabled:t,style:{cursor:t?"not-allowed":"pointer"},onClick:()=>{var t;null==(t=T.current)||t.click(),V(!1)},children:[ze(ye,{className:"text-[var(--chat-ui-text-color)]"}),ze("input",{ref:T,type:"file",accept:"*/*",style:{display:"none"},onChange:t=>{var e;const n=null==(e=t.target.files)?void 0:e[0];n&&zt(n,Y)}})]})}):null,A?ze(ce,{content:"Clear History",children:ze("button",{className:"ai-app-input-section-menu-button",disabled:t,style:{cursor:t?"not-allowed":"pointer"},onClick:U,children:ze(xe,{color:"rgb(250, 132, 132)"})})}):null,o.customFeatures]}),o.customSendButton?o.customSendButton:ze("div",{className:"flex gap-2 items-center",children:ze("button",{className:ke("ai-app-send-button",D?"stop":"send"),style:{cursor:t?"not-allowed":"pointer"},disabled:t||q,onClick:Z,children:D?ze(me,{size:18}):ze(fe,{size:20})})})]})]})})})));import{useMemo as Ye,useCallback as Se,memo as je,useRef as Ce,forwardRef as Fe,useImperativeHandle as Me}from"react";import Ie from"classnames";import{useAtom as Te}from"jotai";M('@import url(./preflight.css);\n.\\!container{\n width: 100% !important;\n}\n.container{\n width: 100%;\n}\n@media (min-width: 640px){\n .\\!container{\n max-width: 640px !important;\n }\n .container{\n max-width: 640px;\n }\n}\n@media (min-width: 768px){\n .\\!container{\n max-width: 768px !important;\n }\n .container{\n max-width: 768px;\n }\n}\n@media (min-width: 1024px){\n .\\!container{\n max-width: 1024px !important;\n }\n .container{\n max-width: 1024px;\n }\n}\n@media (min-width: 1280px){\n .\\!container{\n max-width: 1280px !important;\n }\n .container{\n max-width: 1280px;\n }\n}\n@media (min-width: 1536px){\n .\\!container{\n max-width: 1536px !important;\n }\n .container{\n max-width: 1536px;\n }\n}\n.sr-only{\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n.pointer-events-none{\n pointer-events: none;\n}\n.pointer-events-auto{\n pointer-events: auto;\n}\n.visible{\n visibility: visible;\n}\n.invisible{\n visibility: hidden;\n}\n.static{\n position: static;\n}\n.fixed{\n position: fixed;\n}\n.absolute{\n position: absolute;\n}\n.relative{\n position: relative;\n}\n.sticky{\n position: sticky;\n}\n.inset-0{\n inset: 0px;\n}\n.inset-x-0{\n left: 0px;\n right: 0px;\n}\n.bottom-0{\n bottom: 0px;\n}\n.bottom-\\[10\\%\\]{\n bottom: 10%;\n}\n.bottom-\\[5\\%\\]{\n bottom: 5%;\n}\n.end-3{\n inset-inline-end: 0.75rem;\n}\n.end-auto{\n inset-inline-end: auto;\n}\n.left-0{\n left: 0px;\n}\n.left-1\\.5{\n left: 0.375rem;\n}\n.left-1\\/2{\n left: 50%;\n}\n.left-\\[10\\%\\]{\n left: 10%;\n}\n.left-\\[5\\%\\]{\n left: 5%;\n}\n.right-1{\n right: 0.25rem;\n}\n.right-1\\.5{\n right: 0.375rem;\n}\n.right-\\[10\\%\\]{\n right: 10%;\n}\n.right-\\[5\\%\\]{\n right: 5%;\n}\n.start-2{\n inset-inline-start: 0.5rem;\n}\n.start-3{\n inset-inline-start: 0.75rem;\n}\n.start-auto{\n inset-inline-start: auto;\n}\n.top-0{\n top: 0px;\n}\n.top-1{\n top: 0.25rem;\n}\n.top-1\\/2{\n top: 50%;\n}\n.top-\\[10\\%\\]{\n top: 10%;\n}\n.top-\\[5\\%\\]{\n top: 5%;\n}\n.top-\\[calc\\(100\\%_\\+_2px\\)\\]{\n top: calc(100% + 2px);\n}\n.top-\\[var\\(--navbar-height\\)\\]{\n top: var(--navbar-height);\n}\n.-z-30{\n z-index: -30;\n}\n.z-0{\n z-index: 0;\n}\n.z-10{\n z-index: 10;\n}\n.z-20{\n z-index: 20;\n}\n.z-30{\n z-index: 30;\n}\n.z-40{\n z-index: 40;\n}\n.z-50{\n z-index: 50;\n}\n.order-1{\n order: 1;\n}\n.order-2{\n order: 2;\n}\n.order-3{\n order: 3;\n}\n.-m-2{\n margin: -0.5rem;\n}\n.-m-2\\.5{\n margin: -0.625rem;\n}\n.m-0{\n margin: 0px;\n}\n.-mx-1{\n margin-left: -0.25rem;\n margin-right: -0.25rem;\n}\n.mx-0{\n margin-left: 0px;\n margin-right: 0px;\n}\n.mx-1{\n margin-left: 0.25rem;\n margin-right: 0.25rem;\n}\n.mx-2{\n margin-left: 0.5rem;\n margin-right: 0.5rem;\n}\n.mx-\\[calc\\(\\(theme\\(spacing\\.5\\)-theme\\(spacing\\.1\\)\\)\\/2\\)\\]{\n margin-left: calc((1.25rem - 0.25rem) / 2);\n margin-right: calc((1.25rem - 0.25rem) / 2);\n}\n.mx-\\[calc\\(\\(theme\\(spacing\\.6\\)-theme\\(spacing\\.3\\)\\)\\/2\\)\\]{\n margin-left: calc((1.5rem - 0.75rem) / 2);\n margin-right: calc((1.5rem - 0.75rem) / 2);\n}\n.mx-\\[calc\\(\\(theme\\(spacing\\.7\\)-theme\\(spacing\\.5\\)\\)\\/2\\)\\]{\n margin-left: calc((1.75rem - 1.25rem) / 2);\n margin-right: calc((1.75rem - 1.25rem) / 2);\n}\n.my-0{\n margin-top: 0px;\n margin-bottom: 0px;\n}\n.my-1{\n margin-top: 0.25rem;\n margin-bottom: 0.25rem;\n}\n.my-16{\n margin-top: 4rem;\n margin-bottom: 4rem;\n}\n.my-\\[calc\\(\\(theme\\(spacing\\.5\\)-theme\\(spacing\\.1\\)\\)\\/2\\)\\]{\n margin-top: calc((1.25rem - 0.25rem) / 2);\n margin-bottom: calc((1.25rem - 0.25rem) / 2);\n}\n.my-\\[calc\\(\\(theme\\(spacing\\.6\\)-theme\\(spacing\\.3\\)\\)\\/2\\)\\]{\n margin-top: calc((1.5rem - 0.75rem) / 2);\n margin-bottom: calc((1.5rem - 0.75rem) / 2);\n}\n.my-\\[calc\\(\\(theme\\(spacing\\.7\\)-theme\\(spacing\\.5\\)\\)\\/2\\)\\]{\n margin-top: calc((1.75rem - 1.25rem) / 2);\n margin-bottom: calc((1.75rem - 1.25rem) / 2);\n}\n.my-auto{\n margin-top: auto;\n margin-bottom: auto;\n}\n.-mr-2{\n margin-right: -0.5rem;\n}\n.-ms-2{\n margin-inline-start: -0.5rem;\n}\n.mb-1\\.5{\n margin-bottom: 0.375rem;\n}\n.mb-2{\n margin-bottom: 0.5rem;\n}\n.mb-5{\n margin-bottom: 1.25rem;\n}\n.mb-px{\n margin-bottom: 1px;\n}\n.ml-1{\n margin-left: 0.25rem;\n}\n.ml-2{\n margin-left: 0.5rem;\n}\n.mr-2{\n margin-right: 0.5rem;\n}\n.ms-2{\n margin-inline-start: 0.5rem;\n}\n.mt-1{\n margin-top: 0.25rem;\n}\n.mt-2{\n margin-top: 0.5rem;\n}\n.box-border{\n box-sizing: border-box;\n}\n.box-content{\n box-sizing: content-box;\n}\n.line-clamp-1{\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 1;\n}\n.block{\n display: block;\n}\n.inline-block{\n display: inline-block;\n}\n.inline{\n display: inline;\n}\n.flex{\n display: flex;\n}\n.inline-flex{\n display: inline-flex;\n}\n.table{\n display: table;\n}\n.grid{\n display: grid;\n}\n.inline-grid{\n display: inline-grid;\n}\n.hidden{\n display: none;\n}\n.\\!h-auto{\n height: auto !important;\n}\n.h-1{\n height: 0.25rem;\n}\n.h-1\\.5{\n height: 0.375rem;\n}\n.h-10{\n height: 2.5rem;\n}\n.h-12{\n height: 3rem;\n}\n.h-14{\n height: 3.5rem;\n}\n.h-16{\n height: 4rem;\n}\n.h-2{\n height: 0.5rem;\n}\n.h-2\\.5{\n height: 0.625rem;\n}\n.h-3{\n height: 0.75rem;\n}\n.h-3\\.5{\n height: 0.875rem;\n}\n.h-4{\n height: 1rem;\n}\n.h-40{\n height: 10rem;\n}\n.h-5{\n height: 1.25rem;\n}\n.h-6{\n height: 1.5rem;\n}\n.h-7{\n height: 1.75rem;\n}\n.h-8{\n height: 2rem;\n}\n.h-9{\n height: 2.25rem;\n}\n.h-\\[--visual-viewport-height\\]{\n height: var(--visual-viewport-height);\n}\n.h-\\[100dvh\\]{\n height: 100dvh;\n}\n.h-\\[2px\\]{\n height: 2px;\n}\n.h-\\[calc\\(100dvh_-_var\\(--navbar-height\\)\\)\\]{\n height: calc(100dvh - var(--navbar-height));\n}\n.h-\\[var\\(--navbar-height\\)\\]{\n height: var(--navbar-height);\n}\n.h-\\[var\\(--picker-height\\)\\]{\n height: var(--picker-height);\n}\n.h-auto{\n height: auto;\n}\n.h-divider{\n height: var(--nextui-divider-weight);\n}\n.h-fit{\n height: fit-content;\n}\n.h-full{\n height: 100%;\n}\n.h-px{\n height: 1px;\n}\n.h-screen{\n height: 100vh;\n}\n.max-h-64{\n max-height: 16rem;\n}\n.max-h-\\[calc\\(100\\%_-_8rem\\)\\]{\n max-height: calc(100% - 8rem);\n}\n.min-h-10{\n min-height: 2.5rem;\n}\n.min-h-12{\n min-height: 3rem;\n}\n.min-h-14{\n min-height: 3.5rem;\n}\n.min-h-16{\n min-height: 4rem;\n}\n.min-h-3{\n min-height: 0.75rem;\n}\n.min-h-3\\.5{\n min-height: 0.875rem;\n}\n.min-h-4{\n min-height: 1rem;\n}\n.min-h-5{\n min-height: 1.25rem;\n}\n.min-h-6{\n min-height: 1.5rem;\n}\n.min-h-7{\n min-height: 1.75rem;\n}\n.min-h-8{\n min-height: 2rem;\n}\n.min-h-\\[100dvh\\]{\n min-height: 100dvh;\n}\n.min-h-\\[32px\\]{\n min-height: 32px;\n}\n.w-0{\n width: 0px;\n}\n.w-1{\n width: 0.25rem;\n}\n.w-1\\.5{\n width: 0.375rem;\n}\n.w-10{\n width: 2.5rem;\n}\n.w-12{\n width: 3rem;\n}\n.w-14{\n width: 3.5rem;\n}\n.w-2{\n width: 0.5rem;\n}\n.w-2\\.5{\n width: 0.625rem;\n}\n.w-3{\n width: 0.75rem;\n}\n.w-3\\.5{\n width: 0.875rem;\n}\n.w-4{\n width: 1rem;\n}\n.w-5{\n width: 1.25rem;\n}\n.w-6{\n width: 1.5rem;\n}\n.w-7{\n width: 1.75rem;\n}\n.w-8{\n width: 2rem;\n}\n.w-9{\n width: 2.25rem;\n}\n.w-\\[80\\%\\]{\n width: 80%;\n}\n.w-\\[calc\\(100\\%_-_16px\\)\\]{\n width: calc(100% - 16px);\n}\n.w-\\[calc\\(100\\%_-_theme\\(spacing\\.6\\)\\)\\]{\n width: calc(100% - 1.5rem);\n}\n.w-\\[calc\\(var\\(--visible-months\\)_\\*_var\\(--calendar-width\\)\\)\\]{\n width: calc(var(--visible-months) * var(--calendar-width));\n}\n.w-auto{\n width: auto;\n}\n.w-divider{\n width: var(--nextui-divider-weight);\n}\n.w-fit{\n width: fit-content;\n}\n.w-full{\n width: 100%;\n}\n.w-max{\n width: max-content;\n}\n.w-px{\n width: 1px;\n}\n.w-screen{\n width: 100vw;\n}\n.min-w-10{\n min-width: 2.5rem;\n}\n.min-w-12{\n min-width: 3rem;\n}\n.min-w-16{\n min-width: 4rem;\n}\n.min-w-20{\n min-width: 5rem;\n}\n.min-w-24{\n min-width: 6rem;\n}\n.min-w-3{\n min-width: 0.75rem;\n}\n.min-w-3\\.5{\n min-width: 0.875rem;\n}\n.min-w-4{\n min-width: 1rem;\n}\n.min-w-5{\n min-width: 1.25rem;\n}\n.min-w-6{\n min-width: 1.5rem;\n}\n.min-w-7{\n min-width: 1.75rem;\n}\n.min-w-8{\n min-width: 2rem;\n}\n.min-w-9{\n min-width: 2.25rem;\n}\n.min-w-\\[200px\\]{\n min-width: 200px;\n}\n.min-w-full{\n min-width: 100%;\n}\n.min-w-max{\n min-width: max-content;\n}\n.min-w-min{\n min-width: min-content;\n}\n.max-w-2xl{\n max-width: 42rem;\n}\n.max-w-3xl{\n max-width: 48rem;\n}\n.max-w-4xl{\n max-width: 56rem;\n}\n.max-w-5xl{\n max-width: 64rem;\n}\n.max-w-\\[1024px\\]{\n max-width: 1024px;\n}\n.max-w-\\[1280px\\]{\n max-width: 1280px;\n}\n.max-w-\\[1536px\\]{\n max-width: 1536px;\n}\n.max-w-\\[270px\\]{\n max-width: 270px;\n}\n.max-w-\\[640px\\]{\n max-width: 640px;\n}\n.max-w-\\[768px\\]{\n max-width: 768px;\n}\n.max-w-fit{\n max-width: fit-content;\n}\n.max-w-full{\n max-width: 100%;\n}\n.max-w-lg{\n max-width: 32rem;\n}\n.max-w-md{\n max-width: 28rem;\n}\n.max-w-sm{\n max-width: 24rem;\n}\n.max-w-xl{\n max-width: 36rem;\n}\n.max-w-xs{\n max-width: 20rem;\n}\n.flex-1{\n flex: 1 1 0%;\n}\n.flex-auto{\n flex: 1 1 auto;\n}\n.flex-initial{\n flex: 0 1 auto;\n}\n.flex-none{\n flex: none;\n}\n.flex-shrink-0{\n flex-shrink: 0;\n}\n.shrink-0{\n flex-shrink: 0;\n}\n.flex-grow{\n flex-grow: 1;\n}\n.basis-0{\n flex-basis: 0px;\n}\n.table-auto{\n table-layout: auto;\n}\n.table-fixed{\n table-layout: fixed;\n}\n.border-collapse{\n border-collapse: collapse;\n}\n.origin-center{\n transform-origin: center;\n}\n.origin-left{\n transform-origin: left;\n}\n.origin-right{\n transform-origin: right;\n}\n.origin-top{\n transform-origin: top;\n}\n.origin-top-left{\n transform-origin: top left;\n}\n.-translate-x-1\\/2{\n --tw-translate-x: -50%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.-translate-y-1\\/2{\n --tw-translate-y: -50%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.translate-x-1{\n --tw-translate-x: 0.25rem;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.translate-x-1\\/2{\n --tw-translate-x: 50%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.translate-y-1{\n --tw-translate-y: 0.25rem;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.translate-y-1\\/2{\n --tw-translate-y: 50%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.rotate-0{\n --tw-rotate: 0deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.rotate-180{\n --tw-rotate: 180deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.scale-0{\n --tw-scale-x: 0;\n --tw-scale-y: 0;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.scale-100{\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.scale-105{\n --tw-scale-x: 1.05;\n --tw-scale-y: 1.05;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.scale-50{\n --tw-scale-x: .5;\n --tw-scale-y: .5;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.transform{\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n@keyframes drip-expand{\n 0%{\n opacity: 0.2;\n transform: scale(0);\n }\n 100%{\n opacity: 0;\n transform: scale(2);\n }\n}\n.animate-drip-expand{\n animation: drip-expand 420ms linear;\n}\n@keyframes indeterminate-bar{\n 0%{\n transform: translateX(-50%) scaleX(0.2);\n }\n 100%{\n transform: translateX(100%) scaleX(1);\n }\n}\n.animate-indeterminate-bar{\n animation: indeterminate-bar 1.5s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite normal none running;\n}\n.animate-none{\n animation: none;\n}\n@keyframes spinner-spin{\n 0%{\n transform: rotate(0deg);\n }\n 100%{\n transform: rotate(360deg);\n }\n}\n.animate-spinner-ease-spin{\n animation: spinner-spin 0.8s ease infinite;\n}\n@keyframes spinner-spin{\n 0%{\n transform: rotate(0deg);\n }\n 100%{\n transform: rotate(360deg);\n }\n}\n.animate-spinner-linear-spin{\n animation: spinner-spin 0.8s linear infinite;\n}\n.cursor-default{\n cursor: default;\n}\n.cursor-grab{\n cursor: grab;\n}\n.cursor-not-allowed{\n cursor: not-allowed;\n}\n.cursor-pointer{\n cursor: pointer;\n}\n.cursor-text{\n cursor: text;\n}\n.touch-none{\n touch-action: none;\n}\n.select-none{\n user-select: none;\n}\n.resize-none{\n resize: none;\n}\n.snap-y{\n scroll-snap-type: y var(--tw-scroll-snap-strictness);\n}\n.snap-mandatory{\n --tw-scroll-snap-strictness: mandatory;\n}\n.snap-center{\n scroll-snap-align: center;\n}\n.scroll-py-6{\n scroll-padding-top: 1.5rem;\n scroll-padding-bottom: 1.5rem;\n}\n.list-none{\n list-style-type: none;\n}\n.appearance-none{\n appearance: none;\n}\n.grid-cols-4{\n grid-template-columns: repeat(4, minmax(0, 1fr));\n}\n.flex-row{\n flex-direction: row;\n}\n.flex-row-reverse{\n flex-direction: row-reverse;\n}\n.flex-col{\n flex-direction: column;\n}\n.flex-col-reverse{\n flex-direction: column-reverse;\n}\n.flex-wrap{\n flex-wrap: wrap;\n}\n.flex-nowrap{\n flex-wrap: nowrap;\n}\n.place-content-center{\n place-content: center;\n}\n.items-start{\n align-items: flex-start;\n}\n.items-end{\n align-items: flex-end;\n}\n.items-center{\n align-items: center;\n}\n.justify-start{\n justify-content: flex-start;\n}\n.justify-end{\n justify-content: flex-end;\n}\n.justify-center{\n justify-content: center;\n}\n.justify-between{\n justify-content: space-between;\n}\n.\\!gap-0{\n gap: 0px !important;\n}\n.gap-0{\n gap: 0px;\n}\n.gap-0\\.5{\n gap: 0.125rem;\n}\n.gap-1{\n gap: 0.25rem;\n}\n.gap-1\\.5{\n gap: 0.375rem;\n}\n.gap-2{\n gap: 0.5rem;\n}\n.gap-3{\n gap: 0.75rem;\n}\n.gap-4{\n gap: 1rem;\n}\n.gap-x-0\\.5{\n column-gap: 0.125rem;\n}\n.gap-x-2{\n column-gap: 0.5rem;\n}\n.gap-x-6{\n column-gap: 1.5rem;\n}\n.gap-y-1\\.5{\n row-gap: 0.375rem;\n}\n.gap-y-2{\n row-gap: 0.5rem;\n}\n.space-x-0\\.5 > :not([hidden]) ~ :not([hidden]){\n --tw-space-x-reverse: 0;\n margin-right: calc(0.125rem * var(--tw-space-x-reverse));\n margin-left: calc(0.125rem * calc(1 - var(--tw-space-x-reverse)));\n}\n.self-center{\n align-self: center;\n}\n.overflow-auto{\n overflow: auto;\n}\n.overflow-hidden{\n overflow: hidden;\n}\n.overflow-visible{\n overflow: visible;\n}\n.overflow-x-auto{\n overflow-x: auto;\n}\n.overflow-y-auto{\n overflow-y: auto;\n}\n.overflow-y-hidden{\n overflow-y: hidden;\n}\n.overflow-x-scroll{\n overflow-x: scroll;\n}\n.overflow-y-scroll{\n overflow-y: scroll;\n}\n.truncate{\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.text-ellipsis{\n text-overflow: ellipsis;\n}\n.whitespace-normal{\n white-space: normal;\n}\n.whitespace-nowrap{\n white-space: nowrap;\n}\n.break-words{\n overflow-wrap: break-word;\n}\n.\\!rounded-none{\n border-radius: 0px !important;\n}\n.rounded{\n border-radius: 0.25rem;\n}\n.rounded-\\[calc\\(theme\\(borderRadius\\.large\\)\\/1\\.5\\)\\]{\n border-radius: calc(var(--nextui-radius-large) / 1.5);\n}\n.rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\*0\\.5\\)\\]{\n border-radius: calc(var(--nextui-radius-medium) * 0.5);\n}\n.rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\*0\\.6\\)\\]{\n border-radius: calc(var(--nextui-radius-medium) * 0.6);\n}\n.rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\*0\\.7\\)\\]{\n border-radius: calc(var(--nextui-radius-medium) * 0.7);\n}\n.rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\/2\\)\\]{\n border-radius: calc(var(--nextui-radius-medium) / 2);\n}\n.rounded-\\[calc\\(theme\\(borderRadius\\.small\\)\\/2\\)\\]{\n border-radius: calc(var(--nextui-radius-small) / 2);\n}\n.rounded-full{\n border-radius: 9999px;\n}\n.rounded-large{\n border-radius: var(--nextui-radius-large);\n}\n.rounded-md{\n border-radius: 0.375rem;\n}\n.rounded-medium{\n border-radius: var(--nextui-radius-medium);\n}\n.rounded-none{\n border-radius: 0px;\n}\n.rounded-small{\n border-radius: var(--nextui-radius-small);\n}\n.\\!rounded-e-none{\n border-start-end-radius: 0px !important;\n border-end-end-radius: 0px !important;\n}\n.\\!rounded-s-none{\n border-start-start-radius: 0px !important;\n border-end-start-radius: 0px !important;\n}\n.rounded-b-large{\n border-bottom-right-radius: var(--nextui-radius-large);\n border-bottom-left-radius: var(--nextui-radius-large);\n}\n.rounded-b-medium{\n border-bottom-right-radius: var(--nextui-radius-medium);\n border-bottom-left-radius: var(--nextui-radius-medium);\n}\n.rounded-b-small{\n border-bottom-right-radius: var(--nextui-radius-small);\n border-bottom-left-radius: var(--nextui-radius-small);\n}\n.rounded-t-large{\n border-top-left-radius: var(--nextui-radius-large);\n border-top-right-radius: var(--nextui-radius-large);\n}\n.rounded-t-medium{\n border-top-left-radius: var(--nextui-radius-medium);\n border-top-right-radius: var(--nextui-radius-medium);\n}\n.rounded-t-small{\n border-top-left-radius: var(--nextui-radius-small);\n border-top-right-radius: var(--nextui-radius-small);\n}\n.border{\n border-width: 1px;\n}\n.border-0{\n border-width: 0px;\n}\n.border-2{\n border-width: 2px;\n}\n.border-3{\n border-width: 3px;\n}\n.border-medium{\n border-width: var(--nextui-border-width-medium);\n}\n.border-small{\n border-width: var(--nextui-border-width-small);\n}\n.border-x-\\[calc\\(theme\\(spacing\\.5\\)\\/2\\)\\]{\n border-left-width: calc(1.25rem / 2);\n border-right-width: calc(1.25rem / 2);\n}\n.border-x-\\[calc\\(theme\\(spacing\\.6\\)\\/2\\)\\]{\n border-left-width: calc(1.5rem / 2);\n border-right-width: calc(1.5rem / 2);\n}\n.border-x-\\[calc\\(theme\\(spacing\\.7\\)\\/2\\)\\]{\n border-left-width: calc(1.75rem / 2);\n border-right-width: calc(1.75rem / 2);\n}\n.border-y-\\[calc\\(theme\\(spacing\\.5\\)\\/2\\)\\]{\n border-top-width: calc(1.25rem / 2);\n border-bottom-width: calc(1.25rem / 2);\n}\n.border-y-\\[calc\\(theme\\(spacing\\.6\\)\\/2\\)\\]{\n border-top-width: calc(1.5rem / 2);\n border-bottom-width: calc(1.5rem / 2);\n}\n.border-y-\\[calc\\(theme\\(spacing\\.7\\)\\/2\\)\\]{\n border-top-width: calc(1.75rem / 2);\n border-bottom-width: calc(1.75rem / 2);\n}\n.border-b{\n border-bottom-width: 1px;\n}\n.border-b-medium{\n border-bottom-width: var(--nextui-border-width-medium);\n}\n.border-solid{\n border-style: solid;\n}\n.border-dotted{\n border-style: dotted;\n}\n.border-none{\n border-style: none;\n}\n.\\!border-danger{\n --tw-border-opacity: 1 !important;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity))) !important;\n}\n.border-background{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-border-opacity)));\n}\n.border-danger{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.border-default{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-border-opacity)));\n}\n.border-default-200{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-200) / var(--nextui-default-200-opacity, var(--tw-border-opacity)));\n}\n.border-default-300{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-300) / var(--nextui-default-300-opacity, var(--tw-border-opacity)));\n}\n.border-divider{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-divider) / var(--nextui-divider-opacity, var(--tw-border-opacity)));\n}\n.border-foreground{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-border-opacity)));\n}\n.border-primary{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.border-secondary{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.border-success{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.border-transparent{\n border-color: transparent;\n}\n.border-warning{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.border-x-transparent{\n border-left-color: transparent;\n border-right-color: transparent;\n}\n.border-y-transparent{\n border-top-color: transparent;\n border-bottom-color: transparent;\n}\n.border-b-current{\n border-bottom-color: currentColor;\n}\n.border-b-danger{\n --tw-border-opacity: 1;\n border-bottom-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.border-b-default{\n --tw-border-opacity: 1;\n border-bottom-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-border-opacity)));\n}\n.border-b-foreground{\n --tw-border-opacity: 1;\n border-bottom-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-border-opacity)));\n}\n.border-b-primary{\n --tw-border-opacity: 1;\n border-bottom-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.border-b-secondary{\n --tw-border-opacity: 1;\n border-bottom-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.border-b-success{\n --tw-border-opacity: 1;\n border-bottom-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.border-b-warning{\n --tw-border-opacity: 1;\n border-bottom-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.border-b-white{\n --tw-border-opacity: 1;\n border-bottom-color: rgb(255 255 255 / var(--tw-border-opacity, 1));\n}\n.border-l-transparent{\n border-left-color: transparent;\n}\n.border-r-transparent{\n border-right-color: transparent;\n}\n.border-s-danger{\n --tw-border-opacity: 1;\n border-inline-start-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.border-s-foreground{\n --tw-border-opacity: 1;\n border-inline-start-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-border-opacity)));\n}\n.border-s-primary{\n --tw-border-opacity: 1;\n border-inline-start-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.border-s-secondary{\n --tw-border-opacity: 1;\n border-inline-start-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.border-s-success{\n --tw-border-opacity: 1;\n border-inline-start-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.border-s-warning{\n --tw-border-opacity: 1;\n border-inline-start-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.border-t-transparent{\n border-top-color: transparent;\n}\n.\\!bg-danger-50{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity))) !important;\n}\n.bg-background{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-bg-opacity)));\n}\n.bg-background\\/10{\n background-color: hsl(var(--nextui-background) / 0.1);\n}\n.bg-background\\/70{\n background-color: hsl(var(--nextui-background) / 0.7);\n}\n.bg-background\\/80{\n background-color: hsl(var(--nextui-background) / 0.8);\n}\n.bg-content1{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-content1) / var(--nextui-content1-opacity, var(--tw-bg-opacity)));\n}\n.bg-content3{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-content3) / var(--nextui-content3-opacity, var(--tw-bg-opacity)));\n}\n.bg-current{\n background-color: currentColor;\n}\n.bg-danger{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.bg-danger-100{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-100) / var(--nextui-danger-100-opacity, var(--tw-bg-opacity)));\n}\n.bg-danger-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity)));\n}\n.bg-danger\\/20{\n background-color: hsl(var(--nextui-danger) / 0.2);\n}\n.bg-default{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-bg-opacity)));\n}\n.bg-default-100{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.bg-default-200{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-200) / var(--nextui-default-200-opacity, var(--tw-bg-opacity)));\n}\n.bg-default-300\\/50{\n background-color: hsl(var(--nextui-default-300) / 0.5);\n}\n.bg-default-400{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-400) / var(--nextui-default-400-opacity, var(--tw-bg-opacity)));\n}\n.bg-default-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-50) / var(--nextui-default-50-opacity, var(--tw-bg-opacity)));\n}\n.bg-default-500{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-500) / var(--nextui-default-500-opacity, var(--tw-bg-opacity)));\n}\n.bg-default\\/40{\n background-color: hsl(var(--nextui-default) / 0.4);\n}\n.bg-divider{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-divider) / var(--nextui-divider-opacity, var(--tw-bg-opacity)));\n}\n.bg-foreground{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-bg-opacity)));\n}\n.bg-foreground\\/10{\n background-color: hsl(var(--nextui-foreground) / 0.1);\n}\n.bg-overlay\\/30{\n background-color: hsl(var(--nextui-overlay) / 0.3);\n}\n.bg-overlay\\/50{\n background-color: hsl(var(--nextui-overlay) / 0.5);\n}\n.bg-primary{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.bg-primary-100{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary-100) / var(--nextui-primary-100-opacity, var(--tw-bg-opacity)));\n}\n.bg-primary-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary-50) / var(--nextui-primary-50-opacity, var(--tw-bg-opacity)));\n}\n.bg-primary\\/20{\n background-color: hsl(var(--nextui-primary) / 0.2);\n}\n.bg-secondary{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.bg-secondary-100{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary-100) / var(--nextui-secondary-100-opacity, var(--tw-bg-opacity)));\n}\n.bg-secondary-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary-50) / var(--nextui-secondary-50-opacity, var(--tw-bg-opacity)));\n}\n.bg-secondary\\/20{\n background-color: hsl(var(--nextui-secondary) / 0.2);\n}\n.bg-success{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.bg-success-100{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-100) / var(--nextui-success-100-opacity, var(--tw-bg-opacity)));\n}\n.bg-success-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-50) / var(--nextui-success-50-opacity, var(--tw-bg-opacity)));\n}\n.bg-success\\/20{\n background-color: hsl(var(--nextui-success) / 0.2);\n}\n.bg-transparent{\n background-color: transparent;\n}\n.bg-warning{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.bg-warning-100{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-100) / var(--nextui-warning-100-opacity, var(--tw-bg-opacity)));\n}\n.bg-warning-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-50) / var(--nextui-warning-50-opacity, var(--tw-bg-opacity)));\n}\n.bg-warning\\/20{\n background-color: hsl(var(--nextui-warning) / 0.2);\n}\n.bg-white{\n --tw-bg-opacity: 1;\n background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));\n}\n.bg-stripe-gradient{\n background-image: linear-gradient(45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%, transparent 50%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.1) 75%, transparent 75%, transparent);\n}\n.bg-\\[length\\:1\\.25rem_1\\.25rem\\]{\n background-size: 1.25rem 1.25rem;\n}\n.bg-clip-text{\n background-clip: text;\n}\n.stroke-current{\n stroke: currentColor;\n}\n.stroke-default-300\\/50{\n stroke: hsl(var(--nextui-default-300) / 0.5);\n}\n.object-cover{\n object-fit: cover;\n}\n.p-0{\n padding: 0px;\n}\n.p-1{\n padding: 0.25rem;\n}\n.p-2{\n padding: 0.5rem;\n}\n.p-2\\.5{\n padding: 0.625rem;\n}\n.p-3{\n padding: 0.75rem;\n}\n.p-4{\n padding: 1rem;\n}\n.\\!px-1{\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n}\n.px-0{\n padding-left: 0px;\n padding-right: 0px;\n}\n.px-0\\.5{\n padding-left: 0.125rem;\n padding-right: 0.125rem;\n}\n.px-1{\n padding-left: 0.25rem;\n padding-right: 0.25rem;\n}\n.px-1\\.5{\n padding-left: 0.375rem;\n padding-right: 0.375rem;\n}\n.px-2{\n padding-left: 0.5rem;\n padding-right: 0.5rem;\n}\n.px-2\\.5{\n padding-left: 0.625rem;\n padding-right: 0.625rem;\n}\n.px-3{\n padding-left: 0.75rem;\n padding-right: 0.75rem;\n}\n.px-4{\n padding-left: 1rem;\n padding-right: 1rem;\n}\n.px-5{\n padding-left: 1.25rem;\n padding-right: 1.25rem;\n}\n.px-6{\n padding-left: 1.5rem;\n padding-right: 1.5rem;\n}\n.py-0{\n padding-top: 0px;\n padding-bottom: 0px;\n}\n.py-0\\.5{\n padding-top: 0.125rem;\n padding-bottom: 0.125rem;\n}\n.py-1{\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n}\n.py-1\\.5{\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n}\n.py-2{\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n.py-2\\.5{\n padding-top: 0.625rem;\n padding-bottom: 0.625rem;\n}\n.py-3{\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n}\n.py-4{\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n.\\!pb-0{\n padding-bottom: 0px !important;\n}\n.pb-0{\n padding-bottom: 0px;\n}\n.pb-0\\.5{\n padding-bottom: 0.125rem;\n}\n.pb-1{\n padding-bottom: 0.25rem;\n}\n.pb-1\\.5{\n padding-bottom: 0.375rem;\n}\n.pb-2{\n padding-bottom: 0.5rem;\n}\n.pb-4{\n padding-bottom: 1rem;\n}\n.pe-2{\n padding-inline-end: 0.5rem;\n}\n.pl-0\\.5{\n padding-left: 0.125rem;\n}\n.pl-1{\n padding-left: 0.25rem;\n}\n.pr-0\\.5{\n padding-right: 0.125rem;\n}\n.pr-1{\n padding-right: 0.25rem;\n}\n.pr-4{\n padding-right: 1rem;\n}\n.pr-6{\n padding-right: 1.5rem;\n}\n.ps-2{\n padding-inline-start: 0.5rem;\n}\n.pt-0{\n padding-top: 0px;\n}\n.pt-2{\n padding-top: 0.5rem;\n}\n.text-left{\n text-align: left;\n}\n.text-center{\n text-align: center;\n}\n.text-start{\n text-align: start;\n}\n.text-end{\n text-align: end;\n}\n.align-middle{\n vertical-align: middle;\n}\n.font-mono{\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;\n}\n.font-sans{\n font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";\n}\n.text-\\[0\\.55rem\\]{\n font-size: 0.55rem;\n}\n.text-\\[0\\.5rem\\]{\n font-size: 0.5rem;\n}\n.text-\\[0\\.6rem\\]{\n font-size: 0.6rem;\n}\n.text-large{\n font-size: var(--nextui-font-size-large);\n line-height: var(--nextui-line-height-large);\n}\n.text-lg{\n font-size: 1.125rem;\n line-height: 1.75rem;\n}\n.text-medium{\n font-size: var(--nextui-font-size-medium);\n line-height: var(--nextui-line-height-medium);\n}\n.text-sm{\n font-size: 0.875rem;\n line-height: 1.25rem;\n}\n.text-small{\n font-size: var(--nextui-font-size-small);\n line-height: var(--nextui-line-height-small);\n}\n.text-tiny{\n font-size: var(--nextui-font-size-tiny);\n line-height: var(--nextui-line-height-tiny);\n}\n.text-xl{\n font-size: 1.25rem;\n line-height: 1.75rem;\n}\n.font-light{\n font-weight: 300;\n}\n.font-medium{\n font-weight: 500;\n}\n.font-normal{\n font-weight: 400;\n}\n.font-semibold{\n font-weight: 600;\n}\n.tabular-nums{\n --tw-numeric-spacing: tabular-nums;\n font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction);\n}\n.leading-\\[32px\\]{\n line-height: 32px;\n}\n.\\!text-danger{\n --tw-text-opacity: 1 !important;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity))) !important;\n}\n.\\!text-danger-foreground{\n --tw-text-opacity: 1 !important;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity))) !important;\n}\n.text-\\[var\\(--chat-ui-text-color\\)\\]{\n color: var(--chat-ui-text-color);\n}\n.text-background{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-text-opacity)));\n}\n.text-black{\n --tw-text-opacity: 1;\n color: rgb(0 0 0 / var(--tw-text-opacity, 1));\n}\n.text-current{\n color: currentColor;\n}\n.text-danger{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.text-danger-300{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-300) / var(--nextui-danger-300-opacity, var(--tw-text-opacity)));\n}\n.text-danger-800{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-800) / var(--nextui-danger-800-opacity, var(--tw-text-opacity)));\n}\n.text-danger-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.text-danger\\/80{\n color: hsl(var(--nextui-danger) / 0.8);\n}\n.text-default-400{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-400) / var(--nextui-default-400-opacity, var(--tw-text-opacity)));\n}\n.text-default-500{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-500) / var(--nextui-default-500-opacity, var(--tw-text-opacity)));\n}\n.text-default-600{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-600) / var(--nextui-default-600-opacity, var(--tw-text-opacity)));\n}\n.text-default-700{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-700) / var(--nextui-default-700-opacity, var(--tw-text-opacity)));\n}\n.text-default-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-text-opacity)));\n}\n.text-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-text-opacity)));\n}\n.text-foreground-400{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground-400) / var(--nextui-foreground-400-opacity, var(--tw-text-opacity)));\n}\n.text-foreground-500{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground-500) / var(--nextui-foreground-500-opacity, var(--tw-text-opacity)));\n}\n.text-foreground-600{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground-600) / var(--nextui-foreground-600-opacity, var(--tw-text-opacity)));\n}\n.text-foreground\\/50{\n color: hsl(var(--nextui-foreground) / 0.5);\n}\n.text-gray-400{\n --tw-text-opacity: 1;\n color: rgb(156 163 175 / var(--tw-text-opacity, 1));\n}\n.text-inherit{\n color: inherit;\n}\n.text-primary{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-text-opacity)));\n}\n.text-primary-300{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-300) / var(--nextui-primary-300-opacity, var(--tw-text-opacity)));\n}\n.text-primary-700{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-700) / var(--nextui-primary-700-opacity, var(--tw-text-opacity)));\n}\n.text-primary-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.text-primary\\/80{\n color: hsl(var(--nextui-primary) / 0.8);\n}\n.text-secondary{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-text-opacity)));\n}\n.text-secondary-300{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-300) / var(--nextui-secondary-300-opacity, var(--tw-text-opacity)));\n}\n.text-secondary-700{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-700) / var(--nextui-secondary-700-opacity, var(--tw-text-opacity)));\n}\n.text-secondary-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.text-secondary\\/80{\n color: hsl(var(--nextui-secondary) / 0.8);\n}\n.text-success{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-text-opacity)));\n}\n.text-success-400{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-400) / var(--nextui-success-400-opacity, var(--tw-text-opacity)));\n}\n.text-success-600{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-600) / var(--nextui-success-600-opacity, var(--tw-text-opacity)));\n}\n.text-success-800{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-800) / var(--nextui-success-800-opacity, var(--tw-text-opacity)));\n}\n.text-success-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.text-success\\/80{\n color: hsl(var(--nextui-success) / 0.8);\n}\n.text-warning{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-text-opacity)));\n}\n.text-warning-400{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-400) / var(--nextui-warning-400-opacity, var(--tw-text-opacity)));\n}\n.text-warning-600{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-600) / var(--nextui-warning-600-opacity, var(--tw-text-opacity)));\n}\n.text-warning-800{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-800) / var(--nextui-warning-800-opacity, var(--tw-text-opacity)));\n}\n.text-warning-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.text-warning\\/80{\n color: hsl(var(--nextui-warning) / 0.8);\n}\n.text-white{\n --tw-text-opacity: 1;\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\n}\n.underline{\n text-decoration-line: underline;\n}\n.no-underline{\n text-decoration-line: none;\n}\n.underline-offset-4{\n text-underline-offset: 4px;\n}\n.subpixel-antialiased{\n -webkit-font-smoothing: auto;\n -moz-osx-font-smoothing: auto;\n}\n.opacity-0{\n opacity: 0;\n}\n.opacity-100{\n opacity: 1;\n}\n.opacity-30{\n opacity: 0.3;\n}\n.opacity-50{\n opacity: 0.5;\n}\n.opacity-70{\n opacity: 0.7;\n}\n.opacity-75{\n opacity: 0.75;\n}\n.opacity-disabled{\n opacity: var(--nextui-disabled-opacity);\n}\n.shadow{\n --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-\\[0_1px_0px_0_rgba\\(0\\2c 0\\2c 0\\2c 0\\.05\\)\\]{\n --tw-shadow: 0 1px 0px 0 rgba(0,0,0,0.05);\n --tw-shadow-colored: 0 1px 0px 0 var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-\\[0px_20px_20px_0px_rgb\\(0_0_0\\/0\\.05\\)\\]{\n --tw-shadow: 0px 20px 20px 0px rgb(0 0 0/0.05);\n --tw-shadow-colored: 0px 20px 20px 0px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-large{\n --tw-shadow: var(--nextui-box-shadow-large);\n --tw-shadow-colored: var(--nextui-box-shadow-large);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-lg{\n --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-md{\n --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-medium{\n --tw-shadow: var(--nextui-box-shadow-medium);\n --tw-shadow-colored: var(--nextui-box-shadow-medium);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-none{\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-sm{\n --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-small{\n --tw-shadow: var(--nextui-box-shadow-small);\n --tw-shadow-colored: var(--nextui-box-shadow-small);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-black\\/5{\n --tw-shadow-color: rgb(0 0 0 / 0.05);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.shadow-danger\\/40{\n --tw-shadow-color: hsl(var(--nextui-danger) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.shadow-default\\/50{\n --tw-shadow-color: hsl(var(--nextui-default) / 0.5);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.shadow-foreground\\/40{\n --tw-shadow-color: hsl(var(--nextui-foreground) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.shadow-primary\\/40{\n --tw-shadow-color: hsl(var(--nextui-primary) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.shadow-secondary\\/40{\n --tw-shadow-color: hsl(var(--nextui-secondary) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.shadow-success\\/40{\n --tw-shadow-color: hsl(var(--nextui-success) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.shadow-warning\\/40{\n --tw-shadow-color: hsl(var(--nextui-warning) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.\\!outline-none{\n outline: 2px solid transparent !important;\n outline-offset: 2px !important;\n}\n.outline-none{\n outline: 2px solid transparent;\n outline-offset: 2px;\n}\n.ring-1{\n --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);\n box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);\n}\n.ring-2{\n --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);\n box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);\n}\n.ring-background{\n --tw-ring-opacity: 1;\n --tw-ring-color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-ring-opacity)));\n}\n.ring-danger{\n --tw-ring-opacity: 1;\n --tw-ring-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-ring-opacity)));\n}\n.ring-default{\n --tw-ring-opacity: 1;\n --tw-ring-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-ring-opacity)));\n}\n.ring-focus{\n --tw-ring-opacity: 1;\n --tw-ring-color: hsl(var(--nextui-focus) / var(--nextui-focus-opacity, var(--tw-ring-opacity)));\n}\n.ring-primary{\n --tw-ring-opacity: 1;\n --tw-ring-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-ring-opacity)));\n}\n.ring-secondary{\n --tw-ring-opacity: 1;\n --tw-ring-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-ring-opacity)));\n}\n.ring-success{\n --tw-ring-opacity: 1;\n --tw-ring-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-ring-opacity)));\n}\n.ring-transparent{\n --tw-ring-color: transparent;\n}\n.ring-warning{\n --tw-ring-opacity: 1;\n --tw-ring-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-ring-opacity)));\n}\n.ring-offset-2{\n --tw-ring-offset-width: 2px;\n}\n.ring-offset-background{\n --tw-ring-offset-color: hsl(var(--nextui-background) / var(--nextui-background-opacity, 1));\n}\n.blur{\n --tw-blur: blur(8px);\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.blur-lg{\n --tw-blur: blur(16px);\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.saturate-150{\n --tw-saturate: saturate(1.5);\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.filter{\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.backdrop-blur{\n --tw-backdrop-blur: blur(8px);\n -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.backdrop-blur-lg{\n --tw-backdrop-blur: blur(16px);\n -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.backdrop-blur-md{\n --tw-backdrop-blur: blur(12px);\n -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.backdrop-blur-sm{\n --tw-backdrop-blur: blur(4px);\n -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.backdrop-blur-xl{\n --tw-backdrop-blur: blur(24px);\n -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.backdrop-opacity-disabled{\n --tw-backdrop-opacity: opacity(var(--nextui-disabled-opacity));\n -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.backdrop-saturate-150{\n --tw-backdrop-saturate: saturate(1.5);\n -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.\\!transition-none{\n transition-property: none !important;\n}\n.transition{\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.transition-\\[color\\2c opacity\\]{\n transition-property: color,opacity;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.transition-\\[transform\\2c background-color\\2c color\\]{\n transition-property: transform,background-color,color;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.transition-\\[transform\\2c color\\2c left\\2c opacity\\]{\n transition-property: transform,color,left,opacity;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.transition-all{\n transition-property: all;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.transition-colors{\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.transition-none{\n transition-property: none;\n}\n.transition-opacity{\n transition-property: opacity;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.transition-transform{\n transition-property: transform;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.\\!duration-100{\n transition-duration: 100ms !important;\n}\n.\\!duration-150{\n transition-duration: 150ms !important;\n}\n.\\!duration-200{\n transition-duration: 200ms !important;\n}\n.\\!duration-250{\n transition-duration: 250ms !important;\n}\n.\\!duration-300{\n transition-duration: 300ms !important;\n}\n.\\!duration-500{\n transition-duration: 500ms !important;\n}\n.duration-150{\n transition-duration: 150ms;\n}\n.\\!ease-out{\n transition-timing-function: cubic-bezier(0, 0, 0.2, 1) !important;\n}\n.\\!ease-soft-spring{\n transition-timing-function: cubic-bezier(0.155, 1.105, 0.295, 1.12) !important;\n}\n.will-change-auto{\n will-change: auto;\n}\n.will-change-transform{\n will-change: transform;\n}\n:root,.light,[data-theme="light"]{\n color-scheme: light;\n --nextui-background: 0 0% 100%;\n --nextui-foreground-50: 0 0% 98.04%;\n --nextui-foreground-100: 240 4.76% 95.88%;\n --nextui-foreground-200: 240 5.88% 90%;\n --nextui-foreground-300: 240 4.88% 83.92%;\n --nextui-foreground-400: 240 5.03% 64.9%;\n --nextui-foreground-500: 240 3.83% 46.08%;\n --nextui-foreground-600: 240 5.2% 33.92%;\n --nextui-foreground-700: 240 5.26% 26.08%;\n --nextui-foreground-800: 240 3.7% 15.88%;\n --nextui-foreground-900: 240 5.88% 10%;\n --nextui-foreground: 201.81999999999994 24.44% 8.82%;\n --nextui-divider: 0 0% 6.67%;\n --nextui-divider-opacity: 0.15;\n --nextui-focus: 212.01999999999998 100% 46.67%;\n --nextui-overlay: 0 0% 0%;\n --nextui-content1: 0 0% 100%;\n --nextui-content1-foreground: 201.81999999999994 24.44% 8.82%;\n --nextui-content2: 240 4.76% 95.88%;\n --nextui-content2-foreground: 240 3.7% 15.88%;\n --nextui-content3: 240 5.88% 90%;\n --nextui-content3-foreground: 240 5.26% 26.08%;\n --nextui-content4: 240 4.88% 83.92%;\n --nextui-content4-foreground: 240 5.2% 33.92%;\n --nextui-default-50: 0 0% 98.04%;\n --nextui-default-100: 240 4.76% 95.88%;\n --nextui-default-200: 240 5.88% 90%;\n --nextui-default-300: 240 4.88% 83.92%;\n --nextui-default-400: 240 5.03% 64.9%;\n --nextui-default-500: 240 3.83% 46.08%;\n --nextui-default-600: 240 5.2% 33.92%;\n --nextui-default-700: 240 5.26% 26.08%;\n --nextui-default-800: 240 3.7% 15.88%;\n --nextui-default-900: 240 5.88% 10%;\n --nextui-default-foreground: 0 0% 0%;\n --nextui-default: 240 4.88% 83.92%;\n --nextui-primary-50: 212.5 92.31% 94.9%;\n --nextui-primary-100: 211.84000000000003 92.45% 89.61%;\n --nextui-primary-200: 211.84000000000003 92.45% 79.22%;\n --nextui-primary-300: 212.24 92.45% 68.82%;\n --nextui-primary-400: 212.14 92.45% 58.43%;\n --nextui-primary-500: 212.01999999999998 100% 46.67%;\n --nextui-primary-600: 212.14 100% 38.43%;\n --nextui-primary-700: 212.24 100% 28.82%;\n --nextui-primary-800: 211.84000000000003 100% 19.22%;\n --nextui-primary-900: 211.84000000000003 100% 9.61%;\n --nextui-primary-foreground: 0 0% 100%;\n --nextui-primary: 212.01999999999998 100% 46.67%;\n --nextui-secondary-50: 270 61.54% 94.9%;\n --nextui-secondary-100: 270 59.26% 89.41%;\n --nextui-secondary-200: 270 59.26% 78.82%;\n --nextui-secondary-300: 270 59.26% 68.24%;\n --nextui-secondary-400: 270 59.26% 57.65%;\n --nextui-secondary-500: 270 66.67% 47.06%;\n --nextui-secondary-600: 270 66.67% 37.65%;\n --nextui-secondary-700: 270 66.67% 28.24%;\n --nextui-secondary-800: 270 66.67% 18.82%;\n --nextui-secondary-900: 270 66.67% 9.41%;\n --nextui-secondary-foreground: 0 0% 100%;\n --nextui-secondary: 270 66.67% 47.06%;\n --nextui-success-50: 146.66999999999996 64.29% 94.51%;\n --nextui-success-100: 145.71000000000004 61.4% 88.82%;\n --nextui-success-200: 146.2 61.74% 77.45%;\n --nextui-success-300: 145.78999999999996 62.57% 66.47%;\n --nextui-success-400: 146.01 62.45% 55.1%;\n --nextui-success-500: 145.96000000000004 79.46% 43.92%;\n --nextui-success-600: 146.01 79.89% 35.1%;\n --nextui-success-700: 145.78999999999996 79.26% 26.47%;\n --nextui-success-800: 146.2 79.78% 17.45%;\n --nextui-success-900: 145.71000000000004 77.78% 8.82%;\n --nextui-success-foreground: 0 0% 0%;\n --nextui-success: 145.96000000000004 79.46% 43.92%;\n --nextui-warning-50: 54.55000000000001 91.67% 95.29%;\n --nextui-warning-100: 37.139999999999986 91.3% 90.98%;\n --nextui-warning-200: 37.139999999999986 91.3% 81.96%;\n --nextui-warning-300: 36.95999999999998 91.24% 73.14%;\n --nextui-warning-400: 37.00999999999999 91.26% 64.12%;\n --nextui-warning-500: 37.02999999999997 91.27% 55.1%;\n --nextui-warning-600: 37.00999999999999 74.22% 44.12%;\n --nextui-warning-700: 36.95999999999998 73.96% 33.14%;\n --nextui-warning-800: 37.139999999999986 75% 21.96%;\n --nextui-warning-900: 37.139999999999986 75% 10.98%;\n --nextui-warning-foreground: 0 0% 0%;\n --nextui-warning: 37.02999999999997 91.27% 55.1%;\n --nextui-danger-50: 339.13 92% 95.1%;\n --nextui-danger-100: 340 91.84% 90.39%;\n --nextui-danger-200: 339.3299999999999 90% 80.39%;\n --nextui-danger-300: 339.11 90.6% 70.78%;\n --nextui-danger-400: 339 90% 60.78%;\n --nextui-danger-500: 339.20000000000005 90.36% 51.18%;\n --nextui-danger-600: 339 86.54% 40.78%;\n --nextui-danger-700: 339.11 85.99% 30.78%;\n --nextui-danger-800: 339.3299999999999 86.54% 20.39%;\n --nextui-danger-900: 340 84.91% 10.39%;\n --nextui-danger-foreground: 0 0% 100%;\n --nextui-danger: 339.20000000000005 90.36% 51.18%;\n --nextui-divider-weight: 1px;\n --nextui-disabled-opacity: .5;\n --nextui-font-size-tiny: 0.75rem;\n --nextui-font-size-small: 0.875rem;\n --nextui-font-size-medium: 1rem;\n --nextui-font-size-large: 1.125rem;\n --nextui-line-height-tiny: 1rem;\n --nextui-line-height-small: 1.25rem;\n --nextui-line-height-medium: 1.5rem;\n --nextui-line-height-large: 1.75rem;\n --nextui-radius-small: 8px;\n --nextui-radius-medium: 12px;\n --nextui-radius-large: 14px;\n --nextui-border-width-small: 1px;\n --nextui-border-width-medium: 2px;\n --nextui-border-width-large: 3px;\n --nextui-box-shadow-small: 0px 0px 5px 0px rgb(0 0 0 / 0.02), 0px 2px 10px 0px rgb(0 0 0 / 0.06), 0px 0px 1px 0px rgb(0 0 0 / 0.3);\n --nextui-box-shadow-medium: 0px 0px 15px 0px rgb(0 0 0 / 0.03), 0px 2px 30px 0px rgb(0 0 0 / 0.08), 0px 0px 1px 0px rgb(0 0 0 / 0.3);\n --nextui-box-shadow-large: 0px 0px 30px 0px rgb(0 0 0 / 0.04), 0px 30px 60px 0px rgb(0 0 0 / 0.12), 0px 0px 1px 0px rgb(0 0 0 / 0.3);\n --nextui-hover-opacity: .8;\n}\n.dark,[data-theme="dark"]{\n color-scheme: dark;\n --nextui-background: 0 0% 0%;\n --nextui-foreground-50: 240 5.88% 10%;\n --nextui-foreground-100: 240 3.7% 15.88%;\n --nextui-foreground-200: 240 5.26% 26.08%;\n --nextui-foreground-300: 240 5.2% 33.92%;\n --nextui-foreground-400: 240 3.83% 46.08%;\n --nextui-foreground-500: 240 5.03% 64.9%;\n --nextui-foreground-600: 240 4.88% 83.92%;\n --nextui-foreground-700: 240 5.88% 90%;\n --nextui-foreground-800: 240 4.76% 95.88%;\n --nextui-foreground-900: 0 0% 98.04%;\n --nextui-foreground: 210 5.56% 92.94%;\n --nextui-focus: 212.01999999999998 100% 46.67%;\n --nextui-overlay: 0 0% 0%;\n --nextui-divider: 0 0% 100%;\n --nextui-divider-opacity: 0.15;\n --nextui-content1: 240 5.88% 10%;\n --nextui-content1-foreground: 0 0% 98.04%;\n --nextui-content2: 240 3.7% 15.88%;\n --nextui-content2-foreground: 240 4.76% 95.88%;\n --nextui-content3: 240 5.26% 26.08%;\n --nextui-content3-foreground: 240 5.88% 90%;\n --nextui-content4: 240 5.2% 33.92%;\n --nextui-content4-foreground: 240 4.88% 83.92%;\n --nextui-default-50: 240 5.88% 10%;\n --nextui-default-100: 240 3.7% 15.88%;\n --nextui-default-200: 240 5.26% 26.08%;\n --nextui-default-300: 240 5.2% 33.92%;\n --nextui-default-400: 240 3.83% 46.08%;\n --nextui-default-500: 240 5.03% 64.9%;\n --nextui-default-600: 240 4.88% 83.92%;\n --nextui-default-700: 240 5.88% 90%;\n --nextui-default-800: 240 4.76% 95.88%;\n --nextui-default-900: 0 0% 98.04%;\n --nextui-default-foreground: 0 0% 100%;\n --nextui-default: 240 5.26% 26.08%;\n --nextui-primary-50: 211.84000000000003 100% 9.61%;\n --nextui-primary-100: 211.84000000000003 100% 19.22%;\n --nextui-primary-200: 212.24 100% 28.82%;\n --nextui-primary-300: 212.14 100% 38.43%;\n --nextui-primary-400: 212.01999999999998 100% 46.67%;\n --nextui-primary-500: 212.14 92.45% 58.43%;\n --nextui-primary-600: 212.24 92.45% 68.82%;\n --nextui-primary-700: 211.84000000000003 92.45% 79.22%;\n --nextui-primary-800: 211.84000000000003 92.45% 89.61%;\n --nextui-primary-900: 212.5 92.31% 94.9%;\n --nextui-primary-foreground: 0 0% 100%;\n --nextui-primary: 212.01999999999998 100% 46.67%;\n --nextui-secondary-50: 270 66.67% 9.41%;\n --nextui-secondary-100: 270 66.67% 18.82%;\n --nextui-secondary-200: 270 66.67% 28.24%;\n --nextui-secondary-300: 270 66.67% 37.65%;\n --nextui-secondary-400: 270 66.67% 47.06%;\n --nextui-secondary-500: 270 59.26% 57.65%;\n --nextui-secondary-600: 270 59.26% 68.24%;\n --nextui-secondary-700: 270 59.26% 78.82%;\n --nextui-secondary-800: 270 59.26% 89.41%;\n --nextui-secondary-900: 270 61.54% 94.9%;\n --nextui-secondary-foreground: 0 0% 100%;\n --nextui-secondary: 270 59.26% 57.65%;\n --nextui-success-50: 145.71000000000004 77.78% 8.82%;\n --nextui-success-100: 146.2 79.78% 17.45%;\n --nextui-success-200: 145.78999999999996 79.26% 26.47%;\n --nextui-success-300: 146.01 79.89% 35.1%;\n --nextui-success-400: 145.96000000000004 79.46% 43.92%;\n --nextui-success-500: 146.01 62.45% 55.1%;\n --nextui-success-600: 145.78999999999996 62.57% 66.47%;\n --nextui-success-700: 146.2 61.74% 77.45%;\n --nextui-success-800: 145.71000000000004 61.4% 88.82%;\n --nextui-success-900: 146.66999999999996 64.29% 94.51%;\n --nextui-success-foreground: 0 0% 0%;\n --nextui-success: 145.96000000000004 79.46% 43.92%;\n --nextui-warning-50: 37.139999999999986 75% 10.98%;\n --nextui-warning-100: 37.139999999999986 75% 21.96%;\n --nextui-warning-200: 36.95999999999998 73.96% 33.14%;\n --nextui-warning-300: 37.00999999999999 74.22% 44.12%;\n --nextui-warning-400: 37.02999999999997 91.27% 55.1%;\n --nextui-warning-500: 37.00999999999999 91.26% 64.12%;\n --nextui-warning-600: 36.95999999999998 91.24% 73.14%;\n --nextui-warning-700: 37.139999999999986 91.3% 81.96%;\n --nextui-warning-800: 37.139999999999986 91.3% 90.98%;\n --nextui-warning-900: 54.55000000000001 91.67% 95.29%;\n --nextui-warning-foreground: 0 0% 0%;\n --nextui-warning: 37.02999999999997 91.27% 55.1%;\n --nextui-danger-50: 340 84.91% 10.39%;\n --nextui-danger-100: 339.3299999999999 86.54% 20.39%;\n --nextui-danger-200: 339.11 85.99% 30.78%;\n --nextui-danger-300: 339 86.54% 40.78%;\n --nextui-danger-400: 339.20000000000005 90.36% 51.18%;\n --nextui-danger-500: 339 90% 60.78%;\n --nextui-danger-600: 339.11 90.6% 70.78%;\n --nextui-danger-700: 339.3299999999999 90% 80.39%;\n --nextui-danger-800: 340 91.84% 90.39%;\n --nextui-danger-900: 339.13 92% 95.1%;\n --nextui-danger-foreground: 0 0% 100%;\n --nextui-danger: 339.20000000000005 90.36% 51.18%;\n --nextui-divider-weight: 1px;\n --nextui-disabled-opacity: .5;\n --nextui-font-size-tiny: 0.75rem;\n --nextui-font-size-small: 0.875rem;\n --nextui-font-size-medium: 1rem;\n --nextui-font-size-large: 1.125rem;\n --nextui-line-height-tiny: 1rem;\n --nextui-line-height-small: 1.25rem;\n --nextui-line-height-medium: 1.5rem;\n --nextui-line-height-large: 1.75rem;\n --nextui-radius-small: 8px;\n --nextui-radius-medium: 12px;\n --nextui-radius-large: 14px;\n --nextui-border-width-small: 1px;\n --nextui-border-width-medium: 2px;\n --nextui-border-width-large: 3px;\n --nextui-box-shadow-small: 0px 0px 5px 0px rgb(0 0 0 / 0.05), 0px 2px 10px 0px rgb(0 0 0 / 0.2), inset 0px 0px 1px 0px rgb(255 255 255 / 0.15);\n --nextui-box-shadow-medium: 0px 0px 15px 0px rgb(0 0 0 / 0.06), 0px 2px 30px 0px rgb(0 0 0 / 0.22), inset 0px 0px 1px 0px rgb(255 255 255 / 0.15);\n --nextui-box-shadow-large: 0px 0px 30px 0px rgb(0 0 0 / 0.07), 0px 30px 60px 0px rgb(0 0 0 / 0.26), inset 0px 0px 1px 0px rgb(255 255 255 / 0.15);\n --nextui-hover-opacity: .9;\n}\n.tap-highlight-transparent{\n -webkit-tap-highlight-color: transparent;\n}\n.transition-background{\n transition-property: background;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.transition-colors-opacity{\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.transition-height{\n transition-property: height;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.transition-transform-opacity{\n transition-property: transform, opacity;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.transition-transform-background{\n transition-property: transform, background;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.transition-transform-colors{\n transition-property: transform, color, background, background-color, border-color, text-decoration-color, fill, stroke;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.transition-transform-colors-opacity{\n transition-property: transform, color, background, background-color, border-color, text-decoration-color, fill, stroke, opacity;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.scrollbar-hide{\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n.scrollbar-hide::-webkit-scrollbar{\n display: none;\n}\n.\\[--picker-height\\:224px\\]{\n --picker-height: 224px;\n}\n.\\[--scale-enter\\:100\\%\\]{\n --scale-enter: 100%;\n}\n.\\[--scale-exit\\:100\\%\\]{\n --scale-exit: 100%;\n}\n.\\[--scroll-shadow-size\\:100px\\]{\n --scroll-shadow-size: 100px;\n}\n.\\[--slide-enter\\:0px\\]{\n --slide-enter: 0px;\n}\n.\\[--slide-exit\\:80px\\]{\n --slide-exit: 80px;\n}\n.\\[mask-image\\:linear-gradient\\(\\#000\\2c \\#000\\2c transparent_0\\2c \\#000_var\\(--scroll-shadow-size\\)\\2c \\#000_calc\\(100\\%_-_var\\(--scroll-shadow-size\\)\\)\\2c transparent\\)\\]{\n mask-image: linear-gradient(#000,#000,transparent 0,#000 var(--scroll-shadow-size),#000 calc(100% - var(--scroll-shadow-size)),transparent);\n}\n.file\\:cursor-pointer::file-selector-button{\n cursor: pointer;\n}\n.file\\:border-0::file-selector-button{\n border-width: 0px;\n}\n.file\\:bg-transparent::file-selector-button{\n background-color: transparent;\n}\n.placeholder\\:text-danger::placeholder{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.placeholder\\:text-foreground-500::placeholder{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground-500) / var(--nextui-foreground-500-opacity, var(--tw-text-opacity)));\n}\n.placeholder\\:text-primary::placeholder{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-text-opacity)));\n}\n.placeholder\\:text-secondary::placeholder{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-text-opacity)));\n}\n.placeholder\\:text-success-600::placeholder{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-600) / var(--nextui-success-600-opacity, var(--tw-text-opacity)));\n}\n.placeholder\\:text-warning-600::placeholder{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-600) / var(--nextui-warning-600-opacity, var(--tw-text-opacity)));\n}\n.before\\:absolute::before{\n content: var(--tw-content);\n position: absolute;\n}\n.before\\:inset-0::before{\n content: var(--tw-content);\n inset: 0px;\n}\n.before\\:z-0::before{\n content: var(--tw-content);\n z-index: 0;\n}\n.before\\:z-\\[-1\\]::before{\n content: var(--tw-content);\n z-index: -1;\n}\n.before\\:box-border::before{\n content: var(--tw-content);\n box-sizing: border-box;\n}\n.before\\:block::before{\n content: var(--tw-content);\n display: block;\n}\n.before\\:hidden::before{\n content: var(--tw-content);\n display: none;\n}\n.before\\:h-0\\.5::before{\n content: var(--tw-content);\n height: 0.125rem;\n}\n.before\\:h-11::before{\n content: var(--tw-content);\n height: 2.75rem;\n}\n.before\\:h-2\\.5::before{\n content: var(--tw-content);\n height: 0.625rem;\n}\n.before\\:h-px::before{\n content: var(--tw-content);\n height: 1px;\n}\n.before\\:w-0::before{\n content: var(--tw-content);\n width: 0px;\n}\n.before\\:w-11::before{\n content: var(--tw-content);\n width: 2.75rem;\n}\n.before\\:w-2\\.5::before{\n content: var(--tw-content);\n width: 0.625rem;\n}\n.before\\:w-6::before{\n content: var(--tw-content);\n width: 1.5rem;\n}\n.before\\:-translate-x-full::before{\n content: var(--tw-content);\n --tw-translate-x: -100%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.before\\:-translate-y-1::before{\n content: var(--tw-content);\n --tw-translate-y: -0.25rem;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.before\\:rotate-0::before{\n content: var(--tw-content);\n --tw-rotate: 0deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.before\\:rotate-45::before{\n content: var(--tw-content);\n --tw-rotate: 45deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n@keyframes shimmer{\n 100%{\n content: var(--tw-content);\n transform: translateX(100%);\n }\n}\n.before\\:animate-\\[shimmer_2s_infinite\\]::before{\n content: var(--tw-content);\n animation: shimmer 2s infinite;\n}\n.before\\:animate-none::before{\n content: var(--tw-content);\n animation: none;\n}\n.before\\:rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\*0\\.5\\)\\]::before{\n content: var(--tw-content);\n border-radius: calc(var(--nextui-radius-medium) * 0.5);\n}\n.before\\:rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\*0\\.6\\)\\]::before{\n content: var(--tw-content);\n border-radius: calc(var(--nextui-radius-medium) * 0.6);\n}\n.before\\:rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\*0\\.7\\)\\]::before{\n content: var(--tw-content);\n border-radius: calc(var(--nextui-radius-medium) * 0.7);\n}\n.before\\:rounded-full::before{\n content: var(--tw-content);\n border-radius: 9999px;\n}\n.before\\:rounded-none::before{\n content: var(--tw-content);\n border-radius: 0px;\n}\n.before\\:rounded-sm::before{\n content: var(--tw-content);\n border-radius: 0.125rem;\n}\n.before\\:border-2::before{\n content: var(--tw-content);\n border-width: 2px;\n}\n.before\\:border-t::before{\n content: var(--tw-content);\n border-top-width: 1px;\n}\n.before\\:border-solid::before{\n content: var(--tw-content);\n border-style: solid;\n}\n.before\\:border-content4\\/30::before{\n content: var(--tw-content);\n border-color: hsl(var(--nextui-content4) / 0.3);\n}\n.before\\:border-danger::before{\n content: var(--tw-content);\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.before\\:border-default::before{\n content: var(--tw-content);\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-border-opacity)));\n}\n.before\\:bg-content1::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-content1) / var(--nextui-content1-opacity, var(--tw-bg-opacity)));\n}\n.before\\:bg-current::before{\n content: var(--tw-content);\n background-color: currentColor;\n}\n.before\\:bg-danger::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.before\\:bg-danger\\/20::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-danger) / 0.2);\n}\n.before\\:bg-default\\/60::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-default) / 0.6);\n}\n.before\\:bg-foreground::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-bg-opacity)));\n}\n.before\\:bg-primary::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.before\\:bg-primary\\/20::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-primary) / 0.2);\n}\n.before\\:bg-secondary::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.before\\:bg-secondary\\/20::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-secondary) / 0.2);\n}\n.before\\:bg-success::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.before\\:bg-success\\/20::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-success) / 0.2);\n}\n.before\\:bg-warning::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.before\\:bg-warning\\/20::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-warning) / 0.2);\n}\n.before\\:bg-gradient-to-r::before{\n content: var(--tw-content);\n background-image: linear-gradient(to right, var(--tw-gradient-stops));\n}\n.before\\:from-transparent::before{\n content: var(--tw-content);\n --tw-gradient-from: transparent var(--tw-gradient-from-position);\n --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);\n --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);\n}\n.before\\:via-content4::before{\n content: var(--tw-content);\n --tw-gradient-to: hsl(var(--nextui-content4) / 0) var(--tw-gradient-to-position);\n --tw-gradient-stops: var(--tw-gradient-from), hsl(var(--nextui-content4) / var(--nextui-content4-opacity, 1)) var(--tw-gradient-via-position), var(--tw-gradient-to);\n}\n.before\\:to-transparent::before{\n content: var(--tw-content);\n --tw-gradient-to: transparent var(--tw-gradient-to-position);\n}\n.before\\:opacity-0::before{\n content: var(--tw-content);\n opacity: 0;\n}\n.before\\:opacity-100::before{\n content: var(--tw-content);\n opacity: 1;\n}\n.before\\:shadow-small::before{\n content: var(--tw-content);\n --tw-shadow: var(--nextui-box-shadow-small);\n --tw-shadow-colored: var(--nextui-box-shadow-small);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.before\\:transition-colors::before{\n content: var(--tw-content);\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.before\\:transition-none::before{\n content: var(--tw-content);\n transition-property: none;\n}\n.before\\:transition-transform::before{\n content: var(--tw-content);\n transition-property: transform;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.before\\:duration-150::before{\n content: var(--tw-content);\n transition-duration: 150ms;\n}\n.before\\:content-\\[\\\'\\\'\\]::before{\n --tw-content: \'\';\n content: var(--tw-content);\n}\n.before\\:transition-width::before{\n content: var(--tw-content);\n transition-property: width;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.after\\:absolute::after{\n content: var(--tw-content);\n position: absolute;\n}\n.after\\:inset-0::after{\n content: var(--tw-content);\n inset: 0px;\n}\n.after\\:-bottom-1::after{\n content: var(--tw-content);\n bottom: -0.25rem;\n}\n.after\\:-bottom-\\[2px\\]::after{\n content: var(--tw-content);\n bottom: -2px;\n}\n.after\\:bottom-0::after{\n content: var(--tw-content);\n bottom: 0px;\n}\n.after\\:left-0::after{\n content: var(--tw-content);\n left: 0px;\n}\n.after\\:left-1\\/2::after{\n content: var(--tw-content);\n left: 50%;\n}\n.after\\:right-0::after{\n content: var(--tw-content);\n right: 0px;\n}\n.after\\:top-0::after{\n content: var(--tw-content);\n top: 0px;\n}\n.after\\:-z-10::after{\n content: var(--tw-content);\n z-index: -10;\n}\n.after\\:z-0::after{\n content: var(--tw-content);\n z-index: 0;\n}\n.after\\:ml-0\\.5::after{\n content: var(--tw-content);\n margin-left: 0.125rem;\n}\n.after\\:ms-0\\.5::after{\n content: var(--tw-content);\n margin-inline-start: 0.125rem;\n}\n.after\\:block::after{\n content: var(--tw-content);\n display: block;\n}\n.after\\:h-0::after{\n content: var(--tw-content);\n height: 0px;\n}\n.after\\:h-4::after{\n content: var(--tw-content);\n height: 1rem;\n}\n.after\\:h-5::after{\n content: var(--tw-content);\n height: 1.25rem;\n}\n.after\\:h-\\[2px\\]::after{\n content: var(--tw-content);\n height: 2px;\n}\n.after\\:h-divider::after{\n content: var(--tw-content);\n height: var(--nextui-divider-weight);\n}\n.after\\:h-full::after{\n content: var(--tw-content);\n height: 100%;\n}\n.after\\:h-px::after{\n content: var(--tw-content);\n height: 1px;\n}\n.after\\:w-0::after{\n content: var(--tw-content);\n width: 0px;\n}\n.after\\:w-4::after{\n content: var(--tw-content);\n width: 1rem;\n}\n.after\\:w-5::after{\n content: var(--tw-content);\n width: 1.25rem;\n}\n.after\\:w-6::after{\n content: var(--tw-content);\n width: 1.5rem;\n}\n.after\\:w-\\[80\\%\\]::after{\n content: var(--tw-content);\n width: 80%;\n}\n.after\\:w-full::after{\n content: var(--tw-content);\n width: 100%;\n}\n.after\\:origin-center::after{\n content: var(--tw-content);\n transform-origin: center;\n}\n.after\\:-translate-x-1\\/2::after{\n content: var(--tw-content);\n --tw-translate-x: -50%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.after\\:translate-y-1::after{\n content: var(--tw-content);\n --tw-translate-y: 0.25rem;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.after\\:rotate-0::after{\n content: var(--tw-content);\n --tw-rotate: 0deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.after\\:scale-50::after{\n content: var(--tw-content);\n --tw-scale-x: .5;\n --tw-scale-y: .5;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.after\\:rounded-\\[calc\\(theme\\(borderRadius\\.large\\)\\/2\\)\\]::after{\n content: var(--tw-content);\n border-radius: calc(var(--nextui-radius-large) / 2);\n}\n.after\\:rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\*0\\.5\\)\\]::after{\n content: var(--tw-content);\n border-radius: calc(var(--nextui-radius-medium) * 0.5);\n}\n.after\\:rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\*0\\.6\\)\\]::after{\n content: var(--tw-content);\n border-radius: calc(var(--nextui-radius-medium) * 0.6);\n}\n.after\\:rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\*0\\.7\\)\\]::after{\n content: var(--tw-content);\n border-radius: calc(var(--nextui-radius-medium) * 0.7);\n}\n.after\\:rounded-\\[calc\\(theme\\(borderRadius\\.medium\\)\\/3\\)\\]::after{\n content: var(--tw-content);\n border-radius: calc(var(--nextui-radius-medium) / 3);\n}\n.after\\:rounded-\\[calc\\(theme\\(borderRadius\\.small\\)\\/3\\)\\]::after{\n content: var(--tw-content);\n border-radius: calc(var(--nextui-radius-small) / 3);\n}\n.after\\:rounded-full::after{\n content: var(--tw-content);\n border-radius: 9999px;\n}\n.after\\:rounded-none::after{\n content: var(--tw-content);\n border-radius: 0px;\n}\n.after\\:rounded-xl::after{\n content: var(--tw-content);\n border-radius: 0.75rem;\n}\n.after\\:\\!bg-danger::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity))) !important;\n}\n.after\\:bg-background::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-content1::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-content1) / var(--nextui-content1-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-content3::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-content3) / var(--nextui-content3-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-current::after{\n content: var(--tw-content);\n background-color: currentColor;\n}\n.after\\:bg-danger::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-default::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-default-foreground::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-divider::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-divider) / var(--nextui-divider-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-primary::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-secondary::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-success::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.after\\:bg-warning::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.after\\:text-danger::after{\n content: var(--tw-content);\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.after\\:text-danger-foreground::after{\n content: var(--tw-content);\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.after\\:text-default-foreground::after{\n content: var(--tw-content);\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-text-opacity)));\n}\n.after\\:text-primary-foreground::after{\n content: var(--tw-content);\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.after\\:text-secondary-foreground::after{\n content: var(--tw-content);\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.after\\:text-success-foreground::after{\n content: var(--tw-content);\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.after\\:text-warning-foreground::after{\n content: var(--tw-content);\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.after\\:opacity-0::after{\n content: var(--tw-content);\n opacity: 0;\n}\n.after\\:opacity-100::after{\n content: var(--tw-content);\n opacity: 1;\n}\n.after\\:shadow-\\[0_1px_0px_0_rgba\\(0\\2c 0\\2c 0\\2c 0\\.05\\)\\]::after{\n content: var(--tw-content);\n --tw-shadow: 0 1px 0px 0 rgba(0,0,0,0.05);\n --tw-shadow-colored: 0 1px 0px 0 var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.after\\:shadow-small::after{\n content: var(--tw-content);\n --tw-shadow: var(--nextui-box-shadow-small);\n --tw-shadow-colored: var(--nextui-box-shadow-small);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.after\\:transition-all::after{\n content: var(--tw-content);\n transition-property: all;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.after\\:transition-none::after{\n content: var(--tw-content);\n transition-property: none;\n}\n.after\\:transition-transform::after{\n content: var(--tw-content);\n transition-property: transform;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.after\\:\\!duration-200::after{\n content: var(--tw-content);\n transition-duration: 200ms !important;\n}\n.after\\:duration-150::after{\n content: var(--tw-content);\n transition-duration: 150ms;\n}\n.after\\:\\!ease-linear::after{\n content: var(--tw-content);\n transition-timing-function: linear !important;\n}\n.after\\:content-\\[\\\'\\\'\\]::after{\n --tw-content: \'\';\n content: var(--tw-content);\n}\n.after\\:content-\\[\\\'\\*\\\'\\]::after{\n --tw-content: \'*\';\n content: var(--tw-content);\n}\n.after\\:transition-background::after{\n content: var(--tw-content);\n transition-property: background;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.after\\:transition-width::after{\n content: var(--tw-content);\n transition-property: width;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.after\\:transition-height::after{\n content: var(--tw-content);\n transition-property: height;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.after\\:transition-transform-opacity::after{\n content: var(--tw-content);\n transition-property: transform, opacity;\n transition-timing-function: ease;\n transition-duration: 250ms;\n}\n.first\\:-ml-0\\.5:first-child{\n margin-left: -0.125rem;\n}\n.first\\:mt-2:first-child{\n margin-top: 0.5rem;\n}\n.first\\:rounded-s-full:first-child{\n border-start-start-radius: 9999px;\n border-end-start-radius: 9999px;\n}\n.first\\:rounded-s-large:first-child{\n border-start-start-radius: var(--nextui-radius-large);\n border-end-start-radius: var(--nextui-radius-large);\n}\n.first\\:rounded-s-lg:first-child{\n border-start-start-radius: 0.5rem;\n border-end-start-radius: 0.5rem;\n}\n.first\\:rounded-s-medium:first-child{\n border-start-start-radius: var(--nextui-radius-medium);\n border-end-start-radius: var(--nextui-radius-medium);\n}\n.first\\:rounded-s-none:first-child{\n border-start-start-radius: 0px;\n border-end-start-radius: 0px;\n}\n.first\\:rounded-s-small:first-child{\n border-start-start-radius: var(--nextui-radius-small);\n border-end-start-radius: var(--nextui-radius-small);\n}\n.first\\:before\\:rounded-s-lg:first-child::before{\n content: var(--tw-content);\n border-start-start-radius: 0.5rem;\n border-end-start-radius: 0.5rem;\n}\n.last\\:rounded-e-full:last-child{\n border-start-end-radius: 9999px;\n border-end-end-radius: 9999px;\n}\n.last\\:rounded-e-large:last-child{\n border-start-end-radius: var(--nextui-radius-large);\n border-end-end-radius: var(--nextui-radius-large);\n}\n.last\\:rounded-e-lg:last-child{\n border-start-end-radius: 0.5rem;\n border-end-end-radius: 0.5rem;\n}\n.last\\:rounded-e-medium:last-child{\n border-start-end-radius: var(--nextui-radius-medium);\n border-end-end-radius: var(--nextui-radius-medium);\n}\n.last\\:rounded-e-none:last-child{\n border-start-end-radius: 0px;\n border-end-end-radius: 0px;\n}\n.last\\:rounded-e-small:last-child{\n border-start-end-radius: var(--nextui-radius-small);\n border-end-end-radius: var(--nextui-radius-small);\n}\n.last\\:before\\:rounded-e-lg:last-child::before{\n content: var(--tw-content);\n border-start-end-radius: 0.5rem;\n border-end-end-radius: 0.5rem;\n}\n.first-of-type\\:rounded-e-none:first-of-type{\n border-start-end-radius: 0px;\n border-end-end-radius: 0px;\n}\n.last-of-type\\:rounded-s-none:last-of-type{\n border-start-start-radius: 0px;\n border-end-start-radius: 0px;\n}\n.autofill\\:bg-transparent:autofill{\n background-color: transparent;\n}\n.focus-within\\:border-danger:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:border-default-foreground:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:border-primary:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:border-secondary:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:border-success:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:border-warning:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:bg-danger-50:focus-within{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity)));\n}\n.focus-within\\:bg-primary-50:focus-within{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary-50) / var(--nextui-primary-50-opacity, var(--tw-bg-opacity)));\n}\n.focus-within\\:bg-secondary-50:focus-within{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary-50) / var(--nextui-secondary-50-opacity, var(--tw-bg-opacity)));\n}\n.focus-within\\:bg-success-50:focus-within{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-50) / var(--nextui-success-50-opacity, var(--tw-bg-opacity)));\n}\n.focus-within\\:bg-warning-50:focus-within{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-50) / var(--nextui-warning-50-opacity, var(--tw-bg-opacity)));\n}\n.focus-within\\:after\\:w-full:focus-within::after{\n content: var(--tw-content);\n width: 100%;\n}\n.hover\\:-translate-x-0:hover{\n --tw-translate-x: -0px;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.hover\\:scale-125:hover{\n --tw-scale-x: 1.25;\n --tw-scale-y: 1.25;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.hover\\:border-default:hover{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-border-opacity)));\n}\n.hover\\:border-default-300:hover{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-300) / var(--nextui-default-300-opacity, var(--tw-border-opacity)));\n}\n.hover\\:border-default-400:hover{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-400) / var(--nextui-default-400-opacity, var(--tw-border-opacity)));\n}\n.hover\\:\\!bg-foreground:hover{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-bg-opacity))) !important;\n}\n.hover\\:bg-danger-100:hover{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-100) / var(--nextui-danger-100-opacity, var(--tw-bg-opacity)));\n}\n.hover\\:bg-default-100:hover{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.hover\\:bg-default-200:hover{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-200) / var(--nextui-default-200-opacity, var(--tw-bg-opacity)));\n}\n.hover\\:bg-primary-100:hover{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary-100) / var(--nextui-primary-100-opacity, var(--tw-bg-opacity)));\n}\n.hover\\:bg-secondary-100:hover{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary-100) / var(--nextui-secondary-100-opacity, var(--tw-bg-opacity)));\n}\n.hover\\:bg-success-100:hover{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-100) / var(--nextui-success-100-opacity, var(--tw-bg-opacity)));\n}\n.hover\\:bg-warning-100:hover{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-100) / var(--nextui-warning-100-opacity, var(--tw-bg-opacity)));\n}\n.hover\\:underline:hover{\n text-decoration-line: underline;\n}\n.hover\\:\\!opacity-100:hover{\n opacity: 1 !important;\n}\n.hover\\:opacity-100:hover{\n opacity: 1;\n}\n.hover\\:opacity-80:hover{\n opacity: 0.8;\n}\n.hover\\:after\\:bg-danger\\/20:hover::after{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-danger) / 0.2);\n}\n.hover\\:after\\:bg-foreground\\/10:hover::after{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-foreground) / 0.1);\n}\n.hover\\:after\\:bg-primary\\/20:hover::after{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-primary) / 0.2);\n}\n.hover\\:after\\:bg-secondary\\/20:hover::after{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-secondary) / 0.2);\n}\n.hover\\:after\\:bg-success\\/20:hover::after{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-success) / 0.2);\n}\n.hover\\:after\\:bg-warning\\/20:hover::after{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-warning) / 0.2);\n}\n.hover\\:after\\:opacity-100:hover::after{\n content: var(--tw-content);\n opacity: 1;\n}\n.focus-within\\:hover\\:border-danger:hover:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:hover\\:border-default-foreground:hover:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:hover\\:border-primary:hover:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:hover\\:border-secondary:hover:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:hover\\:border-success:hover:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:hover\\:border-warning:hover:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.focus-within\\:hover\\:bg-default-100:hover:focus-within{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.focus\\:bg-danger-400\\/50:focus{\n background-color: hsl(var(--nextui-danger-400) / 0.5);\n}\n.focus\\:bg-default-400\\/50:focus{\n background-color: hsl(var(--nextui-default-400) / 0.5);\n}\n.focus\\:bg-primary-400\\/50:focus{\n background-color: hsl(var(--nextui-primary-400) / 0.5);\n}\n.focus\\:bg-secondary-400\\/50:focus{\n background-color: hsl(var(--nextui-secondary-400) / 0.5);\n}\n.focus\\:bg-success-400\\/50:focus{\n background-color: hsl(var(--nextui-success-400) / 0.5);\n}\n.focus\\:bg-warning-400\\/50:focus{\n background-color: hsl(var(--nextui-warning-400) / 0.5);\n}\n.focus\\:underline:focus{\n text-decoration-line: underline;\n}\n.focus\\:shadow-sm:focus{\n --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.focus-visible\\:outline-none:focus-visible{\n outline: 2px solid transparent;\n outline-offset: 2px;\n}\n.active\\:bg-default-200:active{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-200) / var(--nextui-default-200-opacity, var(--tw-bg-opacity)));\n}\n.active\\:bg-default-300:active{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-300) / var(--nextui-default-300-opacity, var(--tw-bg-opacity)));\n}\n.active\\:underline:active{\n text-decoration-line: underline;\n}\n.active\\:\\!opacity-70:active{\n opacity: 0.7 !important;\n}\n.active\\:opacity-disabled:active{\n opacity: var(--nextui-disabled-opacity);\n}\n.group:hover .group-hover\\:block{\n display: block;\n}\n.group:hover .group-hover\\:hidden{\n display: none;\n}\n.group:hover .group-hover\\:border-current{\n border-color: currentColor;\n}\n.group:hover .group-hover\\:text-current{\n color: currentColor;\n}\n.aria-expanded\\:scale-\\[0\\.97\\][aria-expanded="true"]{\n --tw-scale-x: 0.97;\n --tw-scale-y: 0.97;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.aria-expanded\\:opacity-70[aria-expanded="true"]{\n opacity: 0.7;\n}\n.data-\\[disabled\\=true\\]\\:pointer-events-none[data-disabled="true"]{\n pointer-events: none;\n}\n.data-\\[loaded\\=true\\]\\:pointer-events-auto[data-loaded="true"]{\n pointer-events: auto;\n}\n.data-\\[visible\\=true\\]\\:pointer-events-auto[data-visible="true"]{\n pointer-events: auto;\n}\n.data-\\[focus-visible\\=true\\]\\:z-10[data-focus-visible="true"]{\n z-index: 10;\n}\n.data-\\[focused\\=true\\]\\:z-10[data-focused="true"]{\n z-index: 10;\n}\n.data-\\[has-label\\=true\\]\\:mt-\\[calc\\(theme\\(fontSize\\.small\\)_\\+_10px\\)\\][data-has-label="true"]{\n margin-top: calc(var(--nextui-font-size-small) + 10px);\n}\n.data-\\[has-label\\=true\\]\\:mt-\\[calc\\(theme\\(fontSize\\.small\\)_\\+_12px\\)\\][data-has-label="true"]{\n margin-top: calc(var(--nextui-font-size-small) + 12px);\n}\n.data-\\[has-label\\=true\\]\\:mt-\\[calc\\(theme\\(fontSize\\.small\\)_\\+_8px\\)\\][data-has-label="true"]{\n margin-top: calc(var(--nextui-font-size-small) + 8px);\n}\n.data-\\[open\\=true\\]\\:block[data-open="true"]{\n display: block;\n}\n.data-\\[open\\=true\\]\\:flex[data-open="true"]{\n display: flex;\n}\n.data-\\[hidden\\=true\\]\\:hidden[data-hidden="true"]{\n display: none;\n}\n.data-\\[inert\\=true\\]\\:hidden[data-inert="true"]{\n display: none;\n}\n.data-\\[justify\\=end\\]\\:flex-grow[data-justify="end"]{\n flex-grow: 1;\n}\n.data-\\[justify\\=start\\]\\:flex-grow[data-justify="start"]{\n flex-grow: 1;\n}\n.data-\\[justify\\=end\\]\\:basis-0[data-justify="end"]{\n flex-basis: 0px;\n}\n.data-\\[justify\\=start\\]\\:basis-0[data-justify="start"]{\n flex-basis: 0px;\n}\n.data-\\[focus-visible\\=true\\]\\:-translate-x-3[data-focus-visible="true"]{\n --tw-translate-x: -0.75rem;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[hover\\=true\\]\\:-translate-x-3[data-hover="true"]{\n --tw-translate-x: -0.75rem;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[hover\\=true\\]\\:translate-x-0[data-hover="true"]{\n --tw-translate-x: 0px;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[before\\=true\\]\\:rotate-180[data-before="true"]{\n --tw-rotate: 180deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[direction\\=ascending\\]\\:rotate-180[data-direction="ascending"]{\n --tw-rotate: 180deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[open\\=true\\]\\:-rotate-90[data-open="true"]{\n --tw-rotate: -90deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[open\\=true\\]\\:rotate-180[data-open="true"]{\n --tw-rotate: 180deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[invisible\\=true\\]\\:scale-0[data-invisible="true"]{\n --tw-scale-x: 0;\n --tw-scale-y: 0;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[pressed\\=true\\]\\:scale-100[data-pressed="true"]{\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[pressed\\=true\\]\\:scale-\\[0\\.97\\][data-pressed="true"]{\n --tw-scale-x: 0.97;\n --tw-scale-y: 0.97;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[disabled\\=true\\]\\:cursor-default[data-disabled="true"]{\n cursor: default;\n}\n.data-\\[disabled\\=true\\]\\:cursor-not-allowed[data-disabled="true"]{\n cursor: not-allowed;\n}\n.data-\\[dragging\\=true\\]\\:cursor-grabbing[data-dragging="true"]{\n cursor: grabbing;\n}\n.data-\\[readonly\\=true\\]\\:cursor-default[data-readonly="true"]{\n cursor: default;\n}\n.data-\\[sortable\\=true\\]\\:cursor-pointer[data-sortable="true"]{\n cursor: pointer;\n}\n.data-\\[unavailable\\=true\\]\\:cursor-default[data-unavailable="true"]{\n cursor: default;\n}\n.data-\\[visible\\=true\\]\\:cursor-pointer[data-visible="true"]{\n cursor: pointer;\n}\n.data-\\[orientation\\=horizontal\\]\\:flex-row[data-orientation="horizontal"]{\n flex-direction: row;\n}\n.data-\\[has-helper\\=true\\]\\:items-start[data-has-helper="true"]{\n align-items: flex-start;\n}\n.data-\\[justify\\=start\\]\\:justify-start[data-justify="start"]{\n justify-content: flex-start;\n}\n.data-\\[justify\\=end\\]\\:justify-end[data-justify="end"]{\n justify-content: flex-end;\n}\n.data-\\[justify\\=center\\]\\:justify-center[data-justify="center"]{\n justify-content: center;\n}\n.data-\\[loaded\\=true\\]\\:overflow-visible[data-loaded="true"]{\n overflow: visible;\n}\n.data-\\[has-multiple-rows\\=true\\]\\:rounded-large[data-has-multiple-rows="true"]{\n border-radius: var(--nextui-radius-large);\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:rounded-full[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n border-radius: 9999px;\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:rounded-full[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n border-radius: 9999px;\n}\n.data-\\[menu-open\\=true\\]\\:border-none[data-menu-open="true"]{\n border-style: none;\n}\n.data-\\[active\\=true\\]\\:border-danger[data-active="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.data-\\[active\\=true\\]\\:border-default-400[data-active="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-400) / var(--nextui-default-400-opacity, var(--tw-border-opacity)));\n}\n.data-\\[active\\=true\\]\\:border-primary[data-active="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[active\\=true\\]\\:border-secondary[data-active="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[active\\=true\\]\\:border-success[data-active="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.data-\\[active\\=true\\]\\:border-warning[data-active="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.data-\\[focus\\=true\\]\\:border-danger[data-focus="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.data-\\[focus\\=true\\]\\:border-default-foreground[data-focus="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-border-opacity)));\n}\n.data-\\[focus\\=true\\]\\:border-primary[data-focus="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[focus\\=true\\]\\:border-secondary[data-focus="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[focus\\=true\\]\\:border-success[data-focus="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.data-\\[focus\\=true\\]\\:border-warning[data-focus="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.data-\\[hover\\=true\\]\\:border-danger[data-hover="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.data-\\[hover\\=true\\]\\:border-default[data-hover="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-border-opacity)));\n}\n.data-\\[hover\\=true\\]\\:border-default-400[data-hover="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-400) / var(--nextui-default-400-opacity, var(--tw-border-opacity)));\n}\n.data-\\[hover\\=true\\]\\:border-primary[data-hover="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[hover\\=true\\]\\:border-secondary[data-hover="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[hover\\=true\\]\\:border-success[data-hover="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.data-\\[hover\\=true\\]\\:border-warning[data-hover="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.data-\\[open\\=true\\]\\:border-danger[data-open="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.data-\\[open\\=true\\]\\:border-default-foreground[data-open="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-border-opacity)));\n}\n.data-\\[open\\=true\\]\\:border-primary[data-open="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[open\\=true\\]\\:border-secondary[data-open="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[open\\=true\\]\\:border-success[data-open="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.data-\\[open\\=true\\]\\:border-warning[data-open="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.data-\\[active\\=true\\]\\:bg-danger[data-active="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[active\\=true\\]\\:bg-default-400[data-active="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-400) / var(--nextui-default-400-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[active\\=true\\]\\:bg-primary[data-active="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[active\\=true\\]\\:bg-secondary[data-active="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[active\\=true\\]\\:bg-success[data-active="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[active\\=true\\]\\:bg-warning[data-active="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:\\!bg-danger[data-hover="true"]{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:\\!bg-danger-100[data-hover="true"]{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-danger-100) / var(--nextui-danger-100-opacity, var(--tw-bg-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:\\!bg-default[data-hover="true"]{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-bg-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:\\!bg-primary[data-hover="true"]{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:\\!bg-secondary[data-hover="true"]{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:\\!bg-success[data-hover="true"]{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:\\!bg-warning[data-hover="true"]{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:bg-content2[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-content2) / var(--nextui-content2-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-danger[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-danger-100[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-100) / var(--nextui-danger-100-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-danger-50[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-danger\\/20[data-hover="true"]{\n background-color: hsl(var(--nextui-danger) / 0.2);\n}\n.data-\\[hover\\=true\\]\\:bg-default[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-default-100[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-default-200[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-200) / var(--nextui-default-200-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-default-50[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-50) / var(--nextui-default-50-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-default\\/40[data-hover="true"]{\n background-color: hsl(var(--nextui-default) / 0.4);\n}\n.data-\\[hover\\=true\\]\\:bg-foreground-200[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-foreground-200) / var(--nextui-foreground-200-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-primary[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-primary-100[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary-100) / var(--nextui-primary-100-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-primary-50[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary-50) / var(--nextui-primary-50-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-primary\\/20[data-hover="true"]{\n background-color: hsl(var(--nextui-primary) / 0.2);\n}\n.data-\\[hover\\=true\\]\\:bg-secondary[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-secondary-100[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary-100) / var(--nextui-secondary-100-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-secondary-50[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary-50) / var(--nextui-secondary-50-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-secondary\\/20[data-hover="true"]{\n background-color: hsl(var(--nextui-secondary) / 0.2);\n}\n.data-\\[hover\\=true\\]\\:bg-success[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-success-100[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-100) / var(--nextui-success-100-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-success-50[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-50) / var(--nextui-success-50-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-success\\/20[data-hover="true"]{\n background-color: hsl(var(--nextui-success) / 0.2);\n}\n.data-\\[hover\\=true\\]\\:bg-transparent[data-hover="true"]{\n background-color: transparent;\n}\n.data-\\[hover\\=true\\]\\:bg-warning[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-warning-100[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-100) / var(--nextui-warning-100-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-warning-50[data-hover="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-50) / var(--nextui-warning-50-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[hover\\=true\\]\\:bg-warning\\/20[data-hover="true"]{\n background-color: hsl(var(--nextui-warning) / 0.2);\n}\n.data-\\[in-range\\=false\\]\\:bg-default-200[data-in-range="false"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-200) / var(--nextui-default-200-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[in-range\\=true\\]\\:bg-background\\/50[data-in-range="true"]{\n background-color: hsl(var(--nextui-background) / 0.5);\n}\n.data-\\[in-range\\=true\\]\\:bg-danger[data-in-range="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[in-range\\=true\\]\\:bg-foreground[data-in-range="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[in-range\\=true\\]\\:bg-primary[data-in-range="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[in-range\\=true\\]\\:bg-secondary[data-in-range="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[in-range\\=true\\]\\:bg-success[data-in-range="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[in-range\\=true\\]\\:bg-warning[data-in-range="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[loaded\\=true\\]\\:\\!bg-transparent[data-loaded="true"]{\n background-color: transparent !important;\n}\n.data-\\[selected\\=true\\]\\:bg-danger[data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:bg-default[data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:bg-foreground[data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:bg-primary[data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:bg-secondary[data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:bg-success[data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:bg-warning[data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:bg-danger[data-hover="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:bg-foreground[data-hover="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:bg-primary[data-hover="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:bg-secondary[data-hover="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:bg-success[data-hover="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:bg-warning[data-hover="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:data-\\[outside-month\\=true\\]\\:bg-transparent[data-outside-month="true"][data-range-selection="true"][data-selected="true"]{\n background-color: transparent;\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-danger[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-foreground[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-primary[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-secondary[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-success[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-warning[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-danger[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-foreground[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-primary[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-secondary[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-success[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:bg-warning[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[has-end-content\\=true\\]\\:pe-1\\.5[data-has-end-content="true"]{\n padding-inline-end: 0.375rem;\n}\n.data-\\[has-helper\\=true\\]\\:pb-\\[calc\\(theme\\(fontSize\\.tiny\\)_\\+8px\\)\\][data-has-helper="true"]{\n padding-bottom: calc(var(--nextui-font-size-tiny) + 8px);\n}\n.data-\\[has-helper\\=true\\]\\:pb-\\[calc\\(theme\\(fontSize\\.tiny\\)_\\+_8px\\)\\][data-has-helper="true"]{\n padding-bottom: calc(var(--nextui-font-size-tiny) + 8px);\n}\n.data-\\[has-start-content\\=true\\]\\:ps-1\\.5[data-has-start-content="true"]{\n padding-inline-start: 0.375rem;\n}\n.data-\\[has-title\\=true\\]\\:pt-1[data-has-title="true"]{\n padding-top: 0.25rem;\n}\n.data-\\[active\\=true\\]\\:font-semibold[data-active="true"]{\n font-weight: 600;\n}\n.data-\\[active\\=true\\]\\:text-danger-foreground[data-active="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[active\\=true\\]\\:text-default-foreground[data-active="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[active\\=true\\]\\:text-primary-foreground[data-active="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[active\\=true\\]\\:text-secondary-foreground[data-active="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[active\\=true\\]\\:text-success-foreground[data-active="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[active\\=true\\]\\:text-warning-foreground[data-active="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[disabled\\=true\\]\\:text-default-300[data-disabled="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-300) / var(--nextui-default-300-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:data-\\[placeholder\\=true\\]\\:text-danger-300[data-placeholder="true"][data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-300) / var(--nextui-danger-300-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:data-\\[placeholder\\=true\\]\\:text-foreground-500[data-placeholder="true"][data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground-500) / var(--nextui-foreground-500-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:data-\\[placeholder\\=true\\]\\:text-primary-300[data-placeholder="true"][data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-300) / var(--nextui-primary-300-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:data-\\[placeholder\\=true\\]\\:text-secondary-300[data-placeholder="true"][data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-300) / var(--nextui-secondary-300-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:data-\\[placeholder\\=true\\]\\:text-success-400[data-placeholder="true"][data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-400) / var(--nextui-success-400-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:data-\\[placeholder\\=true\\]\\:text-warning-400[data-placeholder="true"][data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-400) / var(--nextui-warning-400-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:text-danger[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:text-foreground[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:text-primary[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:text-secondary[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:text-success-600[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-600) / var(--nextui-success-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:text-warning-600[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-600) / var(--nextui-warning-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:\\!text-primary-foreground[data-hover="true"]{\n --tw-text-opacity: 1 !important;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:\\!text-secondary-foreground[data-hover="true"]{\n --tw-text-opacity: 1 !important;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:\\!text-success-foreground[data-hover="true"]{\n --tw-text-opacity: 1 !important;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:\\!text-warning-foreground[data-hover="true"]{\n --tw-text-opacity: 1 !important;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity))) !important;\n}\n.data-\\[hover\\=true\\]\\:text-danger[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-danger-500[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-500) / var(--nextui-danger-500-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-danger-foreground[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-default-500[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-500) / var(--nextui-default-500-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-default-foreground[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-foreground-400[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground-400) / var(--nextui-foreground-400-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-foreground-600[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground-600) / var(--nextui-foreground-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-primary[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-primary-400[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-400) / var(--nextui-primary-400-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-primary-foreground[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-secondary[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-secondary-400[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-400) / var(--nextui-secondary-400-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-secondary-foreground[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-success[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-success-600[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-600) / var(--nextui-success-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-success-foreground[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-warning[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-warning-600[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-600) / var(--nextui-warning-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[hover\\=true\\]\\:text-warning-foreground[data-hover="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[invalid\\=true\\]\\:data-\\[editable\\=true\\]\\:text-danger[data-editable="true"][data-invalid="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.data-\\[invalid\\=true\\]\\:text-danger-300[data-invalid="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-300) / var(--nextui-danger-300-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:text-background[data-hover="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:text-danger-foreground[data-hover="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:text-primary-foreground[data-hover="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:text-secondary-foreground[data-hover="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:text-success-foreground[data-hover="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:text-warning-foreground[data-hover="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:data-\\[outside-month\\=true\\]\\:text-default-300[data-outside-month="true"][data-range-selection="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-300) / var(--nextui-default-300-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-danger-500[data-range-selection="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-500) / var(--nextui-danger-500-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-foreground[data-range-selection="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-primary[data-range-selection="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-secondary[data-range-selection="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-success-600[data-range-selection="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-600) / var(--nextui-success-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-warning-500[data-range-selection="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-500) / var(--nextui-warning-500-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-background[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-danger-foreground[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-primary-foreground[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-secondary-foreground[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-success-foreground[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-warning-foreground[data-range-selection="true"][data-selection-end="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-background[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-danger-foreground[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-primary-foreground[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-secondary-foreground[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-success-foreground[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-warning-foreground[data-range-selection="true"][data-selection-start="true"][data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-background[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-danger[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-danger-foreground[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-default-foreground[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-primary[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-primary-foreground[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-secondary[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-secondary-foreground[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-success-600[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-600) / var(--nextui-success-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-success-foreground[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-warning-600[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-600) / var(--nextui-warning-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selected\\=true\\]\\:text-warning-foreground[data-selected="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[unavailable\\=true\\]\\:text-default-300[data-unavailable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-300) / var(--nextui-default-300-opacity, var(--tw-text-opacity)));\n}\n.data-\\[unavailable\\=true\\]\\:line-through[data-unavailable="true"]{\n text-decoration-line: line-through;\n}\n.data-\\[disabled\\=true\\]\\:data-\\[outside-month\\=true\\]\\:opacity-0[data-outside-month="true"][data-disabled="true"]{\n opacity: 0;\n}\n.data-\\[disabled\\=true\\]\\:opacity-30[data-disabled="true"]{\n opacity: 0.3;\n}\n.data-\\[hover-unselected\\=true\\]\\:opacity-disabled[data-hover-unselected="true"]{\n opacity: var(--nextui-disabled-opacity);\n}\n.data-\\[hover\\=true\\]\\:opacity-hover[data-hover="true"]{\n opacity: var(--nextui-hover-opacity);\n}\n.data-\\[in-range\\=true\\]\\:opacity-100[data-in-range="true"]{\n opacity: 1;\n}\n.data-\\[invisible\\=true\\]\\:opacity-0[data-invisible="true"]{\n opacity: 0;\n}\n.data-\\[loaded\\=true\\]\\:opacity-100[data-loaded="true"]{\n opacity: 1;\n}\n.data-\\[moving\\]\\:opacity-100[data-moving]{\n opacity: 1;\n}\n.data-\\[pressed\\=true\\]\\:opacity-50[data-pressed="true"]{\n opacity: 0.5;\n}\n.data-\\[pressed\\=true\\]\\:opacity-70[data-pressed="true"]{\n opacity: 0.7;\n}\n.data-\\[visible\\=true\\]\\:opacity-100[data-visible="true"]{\n opacity: 1;\n}\n.data-\\[active\\=true\\]\\:shadow-md[data-active="true"]{\n --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.data-\\[hover\\=true\\]\\:shadow-lg[data-hover="true"]{\n --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:shadow-md[data-selection-end="true"][data-selected="true"]{\n --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:shadow-md[data-selection-start="true"][data-selected="true"]{\n --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.data-\\[selected\\=true\\]\\:shadow-md[data-selected="true"]{\n --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.data-\\[selected\\=true\\]\\:shadow-none[data-selected="true"]{\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.data-\\[active\\=true\\]\\:shadow-danger\\/40[data-active="true"]{\n --tw-shadow-color: hsl(var(--nextui-danger) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[active\\=true\\]\\:shadow-default\\/50[data-active="true"]{\n --tw-shadow-color: hsl(var(--nextui-default) / 0.5);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[active\\=true\\]\\:shadow-primary\\/40[data-active="true"]{\n --tw-shadow-color: hsl(var(--nextui-primary) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[active\\=true\\]\\:shadow-secondary\\/40[data-active="true"]{\n --tw-shadow-color: hsl(var(--nextui-secondary) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[active\\=true\\]\\:shadow-success\\/40[data-active="true"]{\n --tw-shadow-color: hsl(var(--nextui-success) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[active\\=true\\]\\:shadow-warning\\/40[data-active="true"]{\n --tw-shadow-color: hsl(var(--nextui-warning) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[hover\\=true\\]\\:shadow-danger\\/30[data-hover="true"]{\n --tw-shadow-color: hsl(var(--nextui-danger) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[hover\\=true\\]\\:shadow-default\\/50[data-hover="true"]{\n --tw-shadow-color: hsl(var(--nextui-default) / 0.5);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[hover\\=true\\]\\:shadow-primary\\/30[data-hover="true"]{\n --tw-shadow-color: hsl(var(--nextui-primary) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[hover\\=true\\]\\:shadow-secondary\\/30[data-hover="true"]{\n --tw-shadow-color: hsl(var(--nextui-secondary) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[hover\\=true\\]\\:shadow-success\\/30[data-hover="true"]{\n --tw-shadow-color: hsl(var(--nextui-success) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[hover\\=true\\]\\:shadow-warning\\/30[data-hover="true"]{\n --tw-shadow-color: hsl(var(--nextui-warning) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selected\\=true\\]\\:shadow-danger\\/40[data-selected="true"]{\n --tw-shadow-color: hsl(var(--nextui-danger) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selected\\=true\\]\\:shadow-foreground\\/40[data-selected="true"]{\n --tw-shadow-color: hsl(var(--nextui-foreground) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selected\\=true\\]\\:shadow-primary\\/40[data-selected="true"]{\n --tw-shadow-color: hsl(var(--nextui-primary) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selected\\=true\\]\\:shadow-secondary\\/40[data-selected="true"]{\n --tw-shadow-color: hsl(var(--nextui-secondary) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selected\\=true\\]\\:shadow-success\\/40[data-selected="true"]{\n --tw-shadow-color: hsl(var(--nextui-success) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selected\\=true\\]\\:shadow-warning\\/40[data-selected="true"]{\n --tw-shadow-color: hsl(var(--nextui-warning) / 0.4);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[focus-visible\\=true\\]\\:outline-2[data-focus-visible="true"]{\n outline-width: 2px;\n}\n.data-\\[focus-visible\\=true\\]\\:outline-offset-2[data-focus-visible="true"]{\n outline-offset: 2px;\n}\n.data-\\[focus-visible\\=true\\]\\:outline-focus[data-focus-visible="true"]{\n outline-color: hsl(var(--nextui-focus) / var(--nextui-focus-opacity, 1));\n}\n.data-\\[focus-visible\\]\\:outline-danger-foreground[data-focus-visible]{\n outline-color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, 1));\n}\n.data-\\[focus-visible\\]\\:outline-default-foreground[data-focus-visible]{\n outline-color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, 1));\n}\n.data-\\[focus-visible\\]\\:outline-primary-foreground[data-focus-visible]{\n outline-color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, 1));\n}\n.data-\\[focus-visible\\]\\:outline-secondary-foreground[data-focus-visible]{\n outline-color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, 1));\n}\n.data-\\[focus-visible\\]\\:outline-success-foreground[data-focus-visible]{\n outline-color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, 1));\n}\n.data-\\[focus-visible\\]\\:outline-warning-foreground[data-focus-visible]{\n outline-color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, 1));\n}\n.data-\\[menu-open\\=true\\]\\:backdrop-blur-xl[data-menu-open="true"]{\n --tw-backdrop-blur: blur(24px);\n -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.data-\\[disabled\\=true\\]\\:transition-none[data-disabled="true"]{\n transition-property: none;\n}\n.data-\\[hover\\=true\\]\\:transition-colors[data-hover="true"]{\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.data-\\[moving\\=true\\]\\:transition-transform[data-moving="true"]{\n transition-property: transform;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n.data-\\[hide-scroll\\=true\\]\\:scrollbar-hide[data-hide-scroll="true"]{\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n.data-\\[hide-scroll\\=true\\]\\:scrollbar-hide[data-hide-scroll="true"]::-webkit-scrollbar{\n display: none;\n}\n.data-\\[top-bottom-scroll\\=true\\]\\:\\[mask-image\\:linear-gradient\\(\\#000\\2c \\#000\\2c transparent_0\\2c \\#000_var\\(--scroll-shadow-size\\)\\2c \\#000_calc\\(100\\%_-_var\\(--scroll-shadow-size\\)\\)\\2c transparent\\)\\][data-top-bottom-scroll="true"]{\n mask-image: linear-gradient(#000,#000,transparent 0,#000 var(--scroll-shadow-size),#000 calc(100% - var(--scroll-shadow-size)),transparent);\n}\n.data-\\[top-scroll\\=true\\]\\:\\[mask-image\\:linear-gradient\\(0deg\\2c \\#000_calc\\(100\\%_-_var\\(--scroll-shadow-size\\)\\)\\2c transparent\\)\\][data-top-scroll="true"]{\n mask-image: linear-gradient(0deg,#000 calc(100% - var(--scroll-shadow-size)),transparent);\n}\n.data-\\[bottom-scroll\\=true\\]\\:\\[mask-image\\:linear-gradient\\(180deg\\2c \\#000_calc\\(100\\%_-_var\\(--scroll-shadow-size\\)\\)\\2c transparent\\)\\][data-bottom-scroll="true"]{\n mask-image: linear-gradient(180deg,#000 calc(100% - var(--scroll-shadow-size)),transparent);\n}\n.data-\\[left-scroll\\=true\\]\\:\\[mask-image\\:linear-gradient\\(270deg\\2c \\#000_calc\\(100\\%_-_var\\(--scroll-shadow-size\\)\\)\\2c transparent\\)\\][data-left-scroll="true"]{\n mask-image: linear-gradient(270deg,#000 calc(100% - var(--scroll-shadow-size)),transparent);\n}\n.data-\\[right-scroll\\=true\\]\\:\\[mask-image\\:linear-gradient\\(90deg\\2c \\#000_calc\\(100\\%_-_var\\(--scroll-shadow-size\\)\\)\\2c transparent\\)\\][data-right-scroll="true"]{\n mask-image: linear-gradient(90deg,#000 calc(100% - var(--scroll-shadow-size)),transparent);\n}\n.data-\\[left-right-scroll\\=true\\]\\:\\[mask-image\\:linear-gradient\\(to_right\\2c \\#000\\2c \\#000\\2c transparent_0\\2c \\#000_var\\(--scroll-shadow-size\\)\\2c \\#000_calc\\(100\\%_-_var\\(--scroll-shadow-size\\)\\)\\2c transparent\\)\\][data-left-right-scroll="true"]{\n mask-image: linear-gradient(to right,#000,#000,transparent 0,#000 var(--scroll-shadow-size),#000 calc(100% - var(--scroll-shadow-size)),transparent);\n}\n.data-\\[placement\\=bottom-end\\]\\:before\\:-top-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_1\\.5px\\)\\][data-placement="bottom-end"]::before{\n content: var(--tw-content);\n top: calc(calc(1.25rem / 4 - 1.5px) * -1);\n}\n.data-\\[placement\\=bottom-end\\]\\:before\\:right-3[data-placement="bottom-end"]::before{\n content: var(--tw-content);\n right: 0.75rem;\n}\n.data-\\[placement\\=bottom-start\\]\\:before\\:-top-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_1\\.5px\\)\\][data-placement="bottom-start"]::before{\n content: var(--tw-content);\n top: calc(calc(1.25rem / 4 - 1.5px) * -1);\n}\n.data-\\[placement\\=bottom-start\\]\\:before\\:left-3[data-placement="bottom-start"]::before{\n content: var(--tw-content);\n left: 0.75rem;\n}\n.data-\\[placement\\=bottom\\]\\:before\\:-top-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_1\\.5px\\)\\][data-placement="bottom"]::before{\n content: var(--tw-content);\n top: calc(calc(1.25rem / 4 - 1.5px) * -1);\n}\n.data-\\[placement\\=bottom\\]\\:before\\:left-1\\/2[data-placement="bottom"]::before{\n content: var(--tw-content);\n left: 50%;\n}\n.data-\\[placement\\=left-end\\]\\:before\\:-right-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_3px\\)\\][data-placement="left-end"]::before{\n content: var(--tw-content);\n right: calc(calc(1.25rem / 4 - 3px) * -1);\n}\n.data-\\[placement\\=left-end\\]\\:before\\:bottom-1\\/4[data-placement="left-end"]::before{\n content: var(--tw-content);\n bottom: 25%;\n}\n.data-\\[placement\\=left-start\\]\\:before\\:-right-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_3px\\)\\][data-placement="left-start"]::before{\n content: var(--tw-content);\n right: calc(calc(1.25rem / 4 - 3px) * -1);\n}\n.data-\\[placement\\=left-start\\]\\:before\\:top-1\\/4[data-placement="left-start"]::before{\n content: var(--tw-content);\n top: 25%;\n}\n.data-\\[placement\\=left\\]\\:before\\:-right-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_2px\\)\\][data-placement="left"]::before{\n content: var(--tw-content);\n right: calc(calc(1.25rem / 4 - 2px) * -1);\n}\n.data-\\[placement\\=left\\]\\:before\\:top-1\\/2[data-placement="left"]::before{\n content: var(--tw-content);\n top: 50%;\n}\n.data-\\[placement\\=right-end\\]\\:before\\:-left-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_3px\\)\\][data-placement="right-end"]::before{\n content: var(--tw-content);\n left: calc(calc(1.25rem / 4 - 3px) * -1);\n}\n.data-\\[placement\\=right-end\\]\\:before\\:bottom-1\\/4[data-placement="right-end"]::before{\n content: var(--tw-content);\n bottom: 25%;\n}\n.data-\\[placement\\=right-start\\]\\:before\\:-left-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_3px\\)\\][data-placement="right-start"]::before{\n content: var(--tw-content);\n left: calc(calc(1.25rem / 4 - 3px) * -1);\n}\n.data-\\[placement\\=right-start\\]\\:before\\:top-1\\/4[data-placement="right-start"]::before{\n content: var(--tw-content);\n top: 25%;\n}\n.data-\\[placement\\=right\\]\\:before\\:-left-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_2px\\)\\][data-placement="right"]::before{\n content: var(--tw-content);\n left: calc(calc(1.25rem / 4 - 2px) * -1);\n}\n.data-\\[placement\\=right\\]\\:before\\:top-1\\/2[data-placement="right"]::before{\n content: var(--tw-content);\n top: 50%;\n}\n.data-\\[placement\\=top-end\\]\\:before\\:-bottom-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_1\\.5px\\)\\][data-placement="top-end"]::before{\n content: var(--tw-content);\n bottom: calc(calc(1.25rem / 4 - 1.5px) * -1);\n}\n.data-\\[placement\\=top-end\\]\\:before\\:right-3[data-placement="top-end"]::before{\n content: var(--tw-content);\n right: 0.75rem;\n}\n.data-\\[placement\\=top-start\\]\\:before\\:-bottom-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_1\\.5px\\)\\][data-placement="top-start"]::before{\n content: var(--tw-content);\n bottom: calc(calc(1.25rem / 4 - 1.5px) * -1);\n}\n.data-\\[placement\\=top-start\\]\\:before\\:left-3[data-placement="top-start"]::before{\n content: var(--tw-content);\n left: 0.75rem;\n}\n.data-\\[placement\\=top\\]\\:before\\:-bottom-\\[calc\\(theme\\(spacing\\.5\\)\\/4_-_1\\.5px\\)\\][data-placement="top"]::before{\n content: var(--tw-content);\n bottom: calc(calc(1.25rem / 4 - 1.5px) * -1);\n}\n.data-\\[placement\\=top\\]\\:before\\:left-1\\/2[data-placement="top"]::before{\n content: var(--tw-content);\n left: 50%;\n}\n.data-\\[loaded\\=true\\]\\:before\\:-z-10[data-loaded="true"]::before{\n content: var(--tw-content);\n z-index: -10;\n}\n.data-\\[arrow\\=true\\]\\:before\\:block[data-arrow="true"]::before{\n content: var(--tw-content);\n display: block;\n}\n.data-\\[outside-month\\=true\\]\\:before\\:hidden[data-outside-month="true"]::before{\n content: var(--tw-content);\n display: none;\n}\n.data-\\[placement\\=bottom\\]\\:before\\:-translate-x-1\\/2[data-placement="bottom"]::before{\n content: var(--tw-content);\n --tw-translate-x: -50%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[placement\\=left\\]\\:before\\:-translate-y-1\\/2[data-placement="left"]::before{\n content: var(--tw-content);\n --tw-translate-y: -50%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[placement\\=right\\]\\:before\\:-translate-y-1\\/2[data-placement="right"]::before{\n content: var(--tw-content);\n --tw-translate-y: -50%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[placement\\=top\\]\\:before\\:-translate-x-1\\/2[data-placement="top"]::before{\n content: var(--tw-content);\n --tw-translate-x: -50%;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[loaded\\=true\\]\\:before\\:animate-none[data-loaded="true"]::before{\n content: var(--tw-content);\n animation: none;\n}\n.data-\\[range-end\\=true\\]\\:before\\:rounded-r-full[data-range-end="true"]::before{\n content: var(--tw-content);\n border-top-right-radius: 9999px;\n border-bottom-right-radius: 9999px;\n}\n.data-\\[range-start\\=true\\]\\:before\\:rounded-l-full[data-range-start="true"]::before{\n content: var(--tw-content);\n border-top-left-radius: 9999px;\n border-bottom-left-radius: 9999px;\n}\n.data-\\[selection-end\\=true\\]\\:before\\:rounded-r-full[data-selection-end="true"]::before{\n content: var(--tw-content);\n border-top-right-radius: 9999px;\n border-bottom-right-radius: 9999px;\n}\n.data-\\[selection-start\\=true\\]\\:before\\:rounded-l-full[data-selection-start="true"]::before{\n content: var(--tw-content);\n border-top-left-radius: 9999px;\n border-bottom-left-radius: 9999px;\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:before\\:bg-danger-50[data-range-selection="true"][data-selected="true"]::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:before\\:bg-foreground\\/10[data-range-selection="true"][data-selected="true"]::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-foreground) / 0.1);\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:before\\:bg-primary-50[data-range-selection="true"][data-selected="true"]::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary-50) / var(--nextui-primary-50-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:before\\:bg-secondary-50[data-range-selection="true"][data-selected="true"]::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary-50) / var(--nextui-secondary-50-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:before\\:bg-success-100[data-range-selection="true"][data-selected="true"]::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-100) / var(--nextui-success-100-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:before\\:bg-warning-100[data-range-selection="true"][data-selected="true"]::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-100) / var(--nextui-warning-100-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[loaded\\=true\\]\\:before\\:opacity-0[data-loaded="true"]::before{\n content: var(--tw-content);\n opacity: 0;\n}\n.data-\\[selected\\=true\\]\\:before\\:opacity-100[data-selected="true"]::before{\n content: var(--tw-content);\n opacity: 1;\n}\n.data-\\[focus\\=true\\]\\:after\\:w-full[data-focus="true"]::after{\n content: var(--tw-content);\n width: 100%;\n}\n.data-\\[open\\=true\\]\\:after\\:w-full[data-open="true"]::after{\n content: var(--tw-content);\n width: 100%;\n}\n.data-\\[dragging\\=true\\]\\:after\\:scale-100[data-dragging="true"]::after{\n content: var(--tw-content);\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[dragging\\=true\\]\\:after\\:scale-80[data-dragging="true"]::after{\n content: var(--tw-content);\n --tw-scale-x: 0.8;\n --tw-scale-y: 0.8;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.data-\\[selected\\=true\\]\\:after\\:bg-danger[data-selected="true"]::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:after\\:bg-foreground[data-selected="true"]::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:after\\:bg-primary[data-selected="true"]::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:after\\:bg-secondary[data-selected="true"]::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:after\\:bg-success[data-selected="true"]::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selected\\=true\\]\\:after\\:bg-warning[data-selected="true"]::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[loaded\\=true\\]\\:after\\:opacity-0[data-loaded="true"]::after{\n content: var(--tw-content);\n opacity: 0;\n}\n.data-\\[selected\\=true\\]\\:after\\:opacity-100[data-selected="true"]::after{\n content: var(--tw-content);\n opacity: 1;\n}\n.data-\\[selectable\\=true\\]\\:focus\\:border-danger:focus[data-selectable="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:border-default:focus[data-selectable="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-border-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:border-primary:focus[data-selectable="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:border-secondary:focus[data-selectable="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:border-success:focus[data-selectable="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:border-warning:focus[data-selectable="true"]{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.data-\\[invalid\\=true\\]\\:focus\\:bg-danger-400\\/50:focus[data-invalid="true"]{\n background-color: hsl(var(--nextui-danger-400) / 0.5);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-danger:focus[data-selectable="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-danger\\/20:focus[data-selectable="true"]{\n background-color: hsl(var(--nextui-danger) / 0.2);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-default:focus[data-selectable="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-default-100:focus[data-selectable="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-default\\/40:focus[data-selectable="true"]{\n background-color: hsl(var(--nextui-default) / 0.4);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-primary:focus[data-selectable="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-primary\\/20:focus[data-selectable="true"]{\n background-color: hsl(var(--nextui-primary) / 0.2);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-secondary:focus[data-selectable="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-secondary\\/20:focus[data-selectable="true"]{\n background-color: hsl(var(--nextui-secondary) / 0.2);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-success:focus[data-selectable="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-success\\/20:focus[data-selectable="true"]{\n background-color: hsl(var(--nextui-success) / 0.2);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-warning:focus[data-selectable="true"]{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:bg-warning\\/20:focus[data-selectable="true"]{\n background-color: hsl(var(--nextui-warning) / 0.2);\n}\n.data-\\[editable\\=true\\]\\:focus\\:text-danger:focus[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:focus\\:text-default-foreground:focus[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:focus\\:text-primary:focus[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:focus\\:text-secondary:focus[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:focus\\:text-success:focus[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:focus\\:text-success-600:focus[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-600) / var(--nextui-success-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:focus\\:text-warning:focus[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-text-opacity)));\n}\n.data-\\[editable\\=true\\]\\:focus\\:text-warning-600:focus[data-editable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-600) / var(--nextui-warning-600-opacity, var(--tw-text-opacity)));\n}\n.data-\\[invalid\\=true\\]\\:data-\\[editable\\=true\\]\\:focus\\:text-danger:focus[data-editable="true"][data-invalid="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-danger:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-danger-foreground:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-default-500:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-500) / var(--nextui-default-500-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-default-foreground:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-primary:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-primary-foreground:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-secondary:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-secondary-foreground:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-success:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-success-foreground:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-warning:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:text-warning-foreground:focus[data-selectable="true"]{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.data-\\[selectable\\=true\\]\\:focus\\:shadow-danger\\/30:focus[data-selectable="true"]{\n --tw-shadow-color: hsl(var(--nextui-danger) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:shadow-default\\/50:focus[data-selectable="true"]{\n --tw-shadow-color: hsl(var(--nextui-default) / 0.5);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:shadow-primary\\/30:focus[data-selectable="true"]{\n --tw-shadow-color: hsl(var(--nextui-primary) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:shadow-secondary\\/30:focus[data-selectable="true"]{\n --tw-shadow-color: hsl(var(--nextui-secondary) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:shadow-success\\/30:focus[data-selectable="true"]{\n --tw-shadow-color: hsl(var(--nextui-success) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.data-\\[selectable\\=true\\]\\:focus\\:shadow-warning\\/30:focus[data-selectable="true"]{\n --tw-shadow-color: hsl(var(--nextui-warning) / 0.3);\n --tw-shadow: var(--tw-shadow-colored);\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:pointer-events-auto{\n pointer-events: auto;\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:start-0{\n inset-inline-start: 0px;\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:start-0{\n inset-inline-start: 0px;\n}\n.group[data-focus-visible="true"] .group-data-\\[focus-visible\\=true\\]\\:z-10{\n z-index: 10;\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:ml-4{\n margin-left: 1rem;\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:ml-5{\n margin-left: 1.25rem;\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:ml-6{\n margin-left: 1.5rem;\n}\n.group[data-selected][data-pressed] .group-data-\\[selected\\]\\:group-data-\\[pressed\\]\\:ml-3{\n margin-left: 0.75rem;\n}\n.group[data-selected][data-pressed] .group-data-\\[selected\\]\\:group-data-\\[pressed\\]\\:ml-4{\n margin-left: 1rem;\n}\n.group[data-selected][data-pressed] .group-data-\\[selected\\]\\:group-data-\\[pressed\\]\\:ml-5{\n margin-left: 1.25rem;\n}\n.group[data-focus-visible="true"] .group-data-\\[focus-visible\\=true\\]\\:block{\n display: block;\n}\n.group[data-has-helper="true"] .group-data-\\[has-helper\\=true\\]\\:flex{\n display: flex;\n}\n.group[data-focus-visible="true"] .group-data-\\[focus-visible\\=true\\]\\:hidden{\n display: none;\n}\n.group[data-pressed="true"] .group-data-\\[pressed\\=true\\]\\:w-5{\n width: 1.25rem;\n}\n.group[data-pressed="true"] .group-data-\\[pressed\\=true\\]\\:w-6{\n width: 1.5rem;\n}\n.group[data-pressed="true"] .group-data-\\[pressed\\=true\\]\\:w-7{\n width: 1.75rem;\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(100\\%_\\+_theme\\(fontSize\\.small\\)\\/2_\\+_20px\\)\\]{\n --tw-translate-y: calc(calc(100% + var(--nextui-font-size-small) / 2 + 20px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(100\\%_\\+_theme\\(fontSize\\.small\\)\\/2_\\+_24px\\)\\]{\n --tw-translate-y: calc(calc(100% + var(--nextui-font-size-small) / 2 + 24px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(100\\%_\\+_theme\\(fontSize\\.tiny\\)\\/2_\\+_16px\\)\\]{\n --tw-translate-y: calc(calc(100% + var(--nextui-font-size-tiny) / 2 + 16px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_3\\.5px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 3.5px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_4px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 4px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_6px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 6px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_6px_-_theme\\(borderWidth\\.medium\\)\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 6px - var(--nextui-border-width-medium)) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_8px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 8px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_8px_-_theme\\(borderWidth\\.medium\\)\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 8px - var(--nextui-border-width-medium)) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.tiny\\)\\/2_-_5px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-tiny) / 2 - 5px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.tiny\\)\\/2_-_8px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-tiny) / 2 - 8px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.tiny\\)\\/2_-_8px_-_theme\\(borderWidth\\.medium\\)\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-tiny) / 2 - 8px - var(--nextui-border-width-medium)) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(100\\%_\\+_theme\\(fontSize\\.small\\)\\/2_\\+_20px\\)\\]{\n --tw-translate-y: calc(calc(100% + var(--nextui-font-size-small) / 2 + 20px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(100\\%_\\+_theme\\(fontSize\\.small\\)\\/2_\\+_24px\\)\\]{\n --tw-translate-y: calc(calc(100% + var(--nextui-font-size-small) / 2 + 24px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(100\\%_\\+_theme\\(fontSize\\.tiny\\)\\/2_\\+_16px\\)\\]{\n --tw-translate-y: calc(calc(100% + var(--nextui-font-size-tiny) / 2 + 16px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_3\\.5px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 3.5px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_4px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 4px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_6px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 6px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_6px_-_theme\\(borderWidth\\.medium\\)\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 6px - var(--nextui-border-width-medium)) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_8px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 8px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.small\\)\\/2_-_8px_-_theme\\(borderWidth\\.medium\\)\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-small) / 2 - 8px - var(--nextui-border-width-medium)) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.tiny\\)\\/2_-_5px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-tiny) / 2 - 5px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.tiny\\)\\/2_-_8px\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-tiny) / 2 - 8px) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:-translate-y-\\[calc\\(50\\%_\\+_theme\\(fontSize\\.tiny\\)\\/2_-_8px_-_theme\\(borderWidth\\.medium\\)\\)\\]{\n --tw-translate-y: calc(calc(50% + var(--nextui-font-size-tiny) / 2 - 8px - var(--nextui-border-width-medium)) * -1);\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:translate-x-3{\n --tw-translate-x: 0.75rem;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-copied="true"] .group-data-\\[copied\\=true\\]\\:scale-100{\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-copied="true"] .group-data-\\[copied\\=true\\]\\:scale-50{\n --tw-scale-x: .5;\n --tw-scale-y: .5;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:scale-85{\n --tw-scale-x: 0.85;\n --tw-scale-y: 0.85;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:scale-85{\n --tw-scale-x: 0.85;\n --tw-scale-y: 0.85;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-pressed="true"] .group-data-\\[pressed\\=true\\]\\:scale-95{\n --tw-scale-x: .95;\n --tw-scale-y: .95;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:scale-100{\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-disabled="true"] .group-data-\\[disabled\\=true\\]\\:cursor-not-allowed{\n cursor: not-allowed;\n}\n.group[data-has-multiple-months="true"] .group-data-\\[has-multiple-months\\=true\\]\\:flex-row{\n flex-direction: row;\n}\n.group[data-has-label="true"] .group-data-\\[has-label\\=true\\]\\:items-start{\n align-items: flex-start;\n}\n.group[data-has-label="true"] .group-data-\\[has-label\\=true\\]\\:items-end{\n align-items: flex-end;\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:\\!border-danger{\n --tw-border-opacity: 1 !important;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity))) !important;\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:border-danger{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:border-default-foreground{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-border-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:border-primary{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:border-secondary{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:border-success{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:border-warning{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.group[data-invalid="true"] .group-data-\\[invalid\\=true\\]\\:border-danger{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:border-danger{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:border-default-500{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-default-500) / var(--nextui-default-500-opacity, var(--tw-border-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:border-primary{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-border-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:border-secondary{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-border-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:border-success{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-border-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:border-warning{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-border-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:\\!bg-danger-50{\n --tw-bg-opacity: 1 !important;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity))) !important;\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:bg-danger-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:bg-default-100{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:bg-primary-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary-50) / var(--nextui-primary-50-opacity, var(--tw-bg-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:bg-secondary-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary-50) / var(--nextui-secondary-50-opacity, var(--tw-bg-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:bg-success-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-50) / var(--nextui-success-50-opacity, var(--tw-bg-opacity)));\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:bg-warning-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-50) / var(--nextui-warning-50-opacity, var(--tw-bg-opacity)));\n}\n.group[data-hover-unselected="true"] .group-data-\\[hover-unselected\\=true\\]\\:bg-default-100{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.group[data-invalid="true"] .group-data-\\[invalid\\=true\\]\\:bg-danger-50{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:bg-danger{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:bg-default-400{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-400) / var(--nextui-default-400-opacity, var(--tw-bg-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:bg-primary{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-bg-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:bg-secondary{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-bg-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:bg-success{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:bg-warning{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.group[data-has-helper="true"] .group-data-\\[has-helper\\=true\\]\\:pt-2{\n padding-top: 0.5rem;\n}\n.group[data-has-helper="true"] .group-data-\\[has-helper\\=true\\]\\:pt-3{\n padding-top: 0.75rem;\n}\n.group[data-has-helper="true"] .group-data-\\[has-helper\\=true\\]\\:pt-4{\n padding-top: 1rem;\n}\n.group[data-has-label="true"] .group-data-\\[has-label\\=true\\]\\:pt-4{\n padding-top: 1rem;\n}\n.group[data-has-label="true"] .group-data-\\[has-label\\=true\\]\\:pt-5{\n padding-top: 1.25rem;\n}\n.group[data-disabled="true"] .group-data-\\[disabled\\=true\\]\\:text-foreground-300{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground-300) / var(--nextui-foreground-300-opacity, var(--tw-text-opacity)));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:text-default-600{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-600) / var(--nextui-default-600-opacity, var(--tw-text-opacity)));\n}\n.group[data-filled-within="true"] .group-data-\\[filled-within\\=true\\]\\:text-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:text-default-600{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-600) / var(--nextui-default-600-opacity, var(--tw-text-opacity)));\n}\n.group[data-filled="true"] .group-data-\\[filled\\=true\\]\\:text-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-has-value="true"] .group-data-\\[has-value\\=true\\]\\:text-default-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-has-value="true"] .group-data-\\[has-value\\=true\\]\\:text-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-invalid="true"] .group-data-\\[invalid\\=true\\]\\:text-danger{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-danger{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-danger-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-default-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-default-foreground) / var(--nextui-default-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-foreground) / var(--nextui-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-primary{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary) / var(--nextui-primary-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-primary-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-primary-foreground) / var(--nextui-primary-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-secondary{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary) / var(--nextui-secondary-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-secondary-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-secondary-foreground) / var(--nextui-secondary-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-success{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-success-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-warning{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:text-warning-foreground{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.group[data-copied="true"] .group-data-\\[copied\\=true\\]\\:opacity-0{\n opacity: 0;\n}\n.group[data-copied="true"] .group-data-\\[copied\\=true\\]\\:opacity-100{\n opacity: 1;\n}\n.group[data-hover="true"] .group-data-\\[hover\\=true\\]\\:opacity-100{\n opacity: 1;\n}\n.group[data-loaded="true"] .group-data-\\[loaded\\=true\\]\\:opacity-100{\n opacity: 1;\n}\n.group[data-pressed="true"] .group-data-\\[pressed\\=true\\]\\:opacity-70{\n opacity: 0.7;\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:opacity-0{\n opacity: 0;\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:opacity-100{\n opacity: 1;\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:opacity-60{\n opacity: 0.6;\n}\n.group[data-focus-visible="true"] .group-data-\\[focus-visible\\=true\\]\\:ring-2{\n --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);\n box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);\n}\n.group[data-focus-visible="true"] .group-data-\\[focus-visible\\=true\\]\\:ring-focus{\n --tw-ring-opacity: 1;\n --tw-ring-color: hsl(var(--nextui-focus) / var(--nextui-focus-opacity, var(--tw-ring-opacity)));\n}\n.group[data-focus-visible="true"] .group-data-\\[focus-visible\\=true\\]\\:ring-offset-2{\n --tw-ring-offset-width: 2px;\n}\n.group[data-focus-visible="true"] .group-data-\\[focus-visible\\=true\\]\\:ring-offset-background{\n --tw-ring-offset-color: hsl(var(--nextui-background) / var(--nextui-background-opacity, 1));\n}\n.group[data-odd="true"] .group-data-\\[odd\\=true\\]\\:before\\:-z-10::before{\n content: var(--tw-content);\n z-index: -10;\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:before\\:w-full::before{\n content: var(--tw-content);\n width: 100%;\n}\n.group[data-open="true"] .group-data-\\[open\\=true\\]\\:before\\:translate-y-px::before{\n content: var(--tw-content);\n --tw-translate-y: 1px;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-open="true"] .group-data-\\[open\\=true\\]\\:before\\:rotate-45::before{\n content: var(--tw-content);\n --tw-rotate: 45deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-middle="true"] .group-data-\\[middle\\=true\\]\\:before\\:rounded-none::before{\n content: var(--tw-content);\n border-radius: 0px;\n}\n.group[data-hover="true"] .group-data-\\[hover\\=true\\]\\:before\\:bg-default-100::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.group[data-odd="true"] .group-data-\\[odd\\=true\\]\\:before\\:bg-default-100::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.group[data-odd="true"] .group-data-\\[odd\\=true\\]\\:before\\:opacity-100::before{\n content: var(--tw-content);\n opacity: 1;\n}\n.group[data-required="true"] .group-data-\\[required\\=true\\]\\:after\\:ml-0\\.5::after{\n content: var(--tw-content);\n margin-left: 0.125rem;\n}\n.group[data-focus="true"] .group-data-\\[focus\\=true\\]\\:after\\:w-full::after{\n content: var(--tw-content);\n width: 100%;\n}\n.group[data-open="true"] .group-data-\\[open\\=true\\]\\:after\\:translate-y-0::after{\n content: var(--tw-content);\n --tw-translate-y: 0px;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-open="true"] .group-data-\\[open\\=true\\]\\:after\\:-rotate-45::after{\n content: var(--tw-content);\n --tw-rotate: -45deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:after\\:scale-100::after{\n content: var(--tw-content);\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-invalid="true"] .group-data-\\[invalid\\=true\\]\\:after\\:bg-danger::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.group[data-required="true"] .group-data-\\[required\\=true\\]\\:after\\:text-danger::after{\n content: var(--tw-content);\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-text-opacity)));\n}\n.group[data-selected="true"] .group-data-\\[selected\\=true\\]\\:after\\:opacity-100::after{\n content: var(--tw-content);\n opacity: 1;\n}\n.group[data-required="true"] .group-data-\\[required\\=true\\]\\:after\\:content-\\[\\\'\\*\\\'\\]::after{\n --tw-content: \'*\';\n content: var(--tw-content);\n}\n.group[data-invalid="true"] .group-data-\\[invalid\\=true\\]\\:hover\\:border-danger:hover{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.group[data-invalid="true"] .group-data-\\[invalid\\=true\\]\\:hover\\:bg-danger-100:hover{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-100) / var(--nextui-danger-100-opacity, var(--tw-bg-opacity)));\n}\n.group[data-invalid="true"] .group-data-\\[invalid\\=true\\]\\:focus-within\\:hover\\:border-danger:hover:focus-within{\n --tw-border-opacity: 1;\n border-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-border-opacity)));\n}\n.group[data-invalid="true"] .group-data-\\[invalid\\=true\\]\\:focus-within\\:hover\\:bg-danger-50:hover:focus-within{\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity)));\n}\n.group[aria-selected="false"][data-hover="true"] .group-aria-\\[selected\\=false\\]\\:group-data-\\[hover\\=true\\]\\:before\\:bg-default-100::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.group[aria-selected="false"][data-hover="true"] .group-aria-\\[selected\\=false\\]\\:group-data-\\[hover\\=true\\]\\:before\\:opacity-70::before{\n content: var(--tw-content);\n opacity: 0.7;\n}\n.group[data-odd="true"] .group-data-\\[odd\\=true\\]\\:data-\\[selected\\=true\\]\\:before\\:bg-danger\\/20[data-selected="true"]::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-danger) / 0.2);\n}\n.group[data-odd="true"] .group-data-\\[odd\\=true\\]\\:data-\\[selected\\=true\\]\\:before\\:bg-default\\/60[data-selected="true"]::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-default) / 0.6);\n}\n.group[data-odd="true"] .group-data-\\[odd\\=true\\]\\:data-\\[selected\\=true\\]\\:before\\:bg-primary\\/20[data-selected="true"]::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-primary) / 0.2);\n}\n.group[data-odd="true"] .group-data-\\[odd\\=true\\]\\:data-\\[selected\\=true\\]\\:before\\:bg-secondary\\/20[data-selected="true"]::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-secondary) / 0.2);\n}\n.group[data-odd="true"] .group-data-\\[odd\\=true\\]\\:data-\\[selected\\=true\\]\\:before\\:bg-success\\/20[data-selected="true"]::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-success) / 0.2);\n}\n.group[data-odd="true"] .group-data-\\[odd\\=true\\]\\:data-\\[selected\\=true\\]\\:before\\:bg-warning\\/20[data-selected="true"]::before{\n content: var(--tw-content);\n background-color: hsl(var(--nextui-warning) / 0.2);\n}\n.peer[data-filled="true"] ~ .peer-data-\\[filled\\=true\\]\\:block{\n display: block;\n}\n.peer[data-filled="true"] ~ .peer-data-\\[filled\\=true\\]\\:opacity-70{\n opacity: 0.7;\n}\n@media (prefers-reduced-motion: reduce){\n .motion-reduce\\:transition-none{\n transition-property: none;\n }\n .motion-reduce\\:after\\:transition-none::after{\n content: var(--tw-content);\n transition-property: none;\n }\n}\n.dark\\:bg-background:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:bg-background\\/20:is(.dark *){\n background-color: hsl(var(--nextui-background) / 0.2);\n}\n.dark\\:bg-content2:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-content2) / var(--nextui-content2-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:bg-default:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default) / var(--nextui-default-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:bg-transparent:is(.dark *){\n background-color: transparent;\n}\n.dark\\:text-danger-500:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-500) / var(--nextui-danger-500-opacity, var(--tw-text-opacity)));\n}\n.dark\\:text-success:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-text-opacity)));\n}\n.dark\\:text-warning:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-text-opacity)));\n}\n.dark\\:placeholder\\:text-danger-500:is(.dark *)::placeholder{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-500) / var(--nextui-danger-500-opacity, var(--tw-text-opacity)));\n}\n.dark\\:placeholder\\:text-success:is(.dark *)::placeholder{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-text-opacity)));\n}\n.dark\\:placeholder\\:text-warning:is(.dark *)::placeholder{\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-text-opacity)));\n}\n.dark\\:before\\:via-default-700\\/10:is(.dark *)::before{\n content: var(--tw-content);\n --tw-gradient-to: hsl(var(--nextui-default-700) / 0) var(--tw-gradient-to-position);\n --tw-gradient-stops: var(--tw-gradient-from), hsl(var(--nextui-default-700) / 0.1) var(--tw-gradient-via-position), var(--tw-gradient-to);\n}\n.dark\\:after\\:bg-content2:is(.dark *)::after{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-content2) / var(--nextui-content2-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:focus\\:bg-danger-400\\/20:focus:is(.dark *){\n background-color: hsl(var(--nextui-danger-400) / 0.2);\n}\n.dark\\:focus\\:bg-success-400\\/20:focus:is(.dark *){\n background-color: hsl(var(--nextui-success-400) / 0.2);\n}\n.dark\\:focus\\:bg-warning-400\\/20:focus:is(.dark *){\n background-color: hsl(var(--nextui-warning-400) / 0.2);\n}\n.dark\\:data-\\[hover\\=true\\]\\:bg-content2[data-hover="true"]:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-content2) / var(--nextui-content2-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:data-\\[hover\\=true\\]\\:bg-danger-50[data-hover="true"]:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger-50) / var(--nextui-danger-50-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:data-\\[hover\\=true\\]\\:bg-success-50[data-hover="true"]:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-50) / var(--nextui-success-50-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:data-\\[hover\\=true\\]\\:bg-warning-50[data-hover="true"]:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-50) / var(--nextui-warning-50-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:bg-danger[data-hover="true"][data-selected="true"]:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-danger) / var(--nextui-danger-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:bg-success[data-hover="true"][data-selected="true"]:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:bg-warning[data-hover="true"][data-selected="true"]:is(.dark *){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:data-\\[hover\\=true\\]\\:text-danger-500[data-hover="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-500) / var(--nextui-danger-500-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[hover\\=true\\]\\:text-success-500[data-hover="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-500) / var(--nextui-success-500-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[hover\\=true\\]\\:text-warning-500[data-hover="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-500) / var(--nextui-warning-500-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:text-danger-foreground[data-hover="true"][data-selected="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-foreground) / var(--nextui-danger-foreground-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:text-success-foreground[data-hover="true"][data-selected="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[hover\\=true\\]\\:text-warning-foreground[data-hover="true"][data-selected="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning-foreground) / var(--nextui-warning-foreground-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-success-500[data-range-selection="true"][data-selected="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-500) / var(--nextui-success-500-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[selection-end\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-success-foreground[data-range-selection="true"][data-selection-end="true"][data-selected="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[selection-start\\=true\\]\\:data-\\[range-selection\\=true\\]\\:text-success-foreground[data-range-selection="true"][data-selection-start="true"][data-selected="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success-foreground) / var(--nextui-success-foreground-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:text-danger-500[data-selected="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-danger-500) / var(--nextui-danger-500-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:text-success[data-selected="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-success) / var(--nextui-success-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:text-warning[data-selected="true"]:is(.dark *){\n --tw-text-opacity: 1;\n color: hsl(var(--nextui-warning) / var(--nextui-warning-opacity, var(--tw-text-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:before\\:bg-success-50[data-range-selection="true"][data-selected="true"]:is(.dark *)::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-success-50) / var(--nextui-success-50-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:data-\\[selected\\=true\\]\\:data-\\[range-selection\\=true\\]\\:before\\:bg-warning-50[data-range-selection="true"][data-selected="true"]:is(.dark *)::before{\n content: var(--tw-content);\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-warning-50) / var(--nextui-warning-50-opacity, var(--tw-bg-opacity)));\n}\n.dark\\:data-\\[invalid\\=true\\]\\:focus\\:bg-danger-400\\/20:focus[data-invalid="true"]:is(.dark *){\n background-color: hsl(var(--nextui-danger-400) / 0.2);\n}\n@media (min-width: 640px){\n .sm\\:mx-0{\n margin-left: 0px;\n margin-right: 0px;\n }\n .sm\\:mx-6{\n margin-left: 1.5rem;\n margin-right: 1.5rem;\n }\n .sm\\:my-0{\n margin-top: 0px;\n margin-bottom: 0px;\n }\n .sm\\:my-16{\n margin-top: 4rem;\n margin-bottom: 4rem;\n }\n .sm\\:items-start{\n align-items: flex-start;\n }\n .sm\\:items-end{\n align-items: flex-end;\n }\n .sm\\:items-center{\n align-items: center;\n }\n .sm\\:\\[--scale-enter\\:100\\%\\]{\n --scale-enter: 100%;\n }\n .sm\\:\\[--scale-exit\\:103\\%\\]{\n --scale-exit: 103%;\n }\n .sm\\:\\[--slide-enter\\:0px\\]{\n --slide-enter: 0px;\n }\n .sm\\:\\[--slide-exit\\:0px\\]{\n --slide-exit: 0px;\n }\n .sm\\:data-\\[visible\\=true\\]\\:pointer-events-none[data-visible="true"]{\n pointer-events: none;\n }\n .sm\\:data-\\[visible\\=true\\]\\:opacity-0[data-visible="true"]{\n opacity: 0;\n }\n .group[data-hover="true"] .sm\\:group-data-\\[hover\\=true\\]\\:data-\\[visible\\=true\\]\\:pointer-events-auto[data-visible="true"]{\n pointer-events: auto;\n }\n .group[data-hover="true"] .sm\\:group-data-\\[hover\\=true\\]\\:data-\\[visible\\=true\\]\\:opacity-100[data-visible="true"]{\n opacity: 1;\n }\n}\n.rtl\\:left-1:where([dir="rtl"], [dir="rtl"] *){\n left: 0.25rem;\n}\n.rtl\\:left-1\\.5:where([dir="rtl"], [dir="rtl"] *){\n left: 0.375rem;\n}\n.rtl\\:left-\\[unset\\]:where([dir="rtl"], [dir="rtl"] *){\n left: unset;\n}\n.rtl\\:right-0:where([dir="rtl"], [dir="rtl"] *){\n right: 0px;\n}\n.rtl\\:right-1\\.5:where([dir="rtl"], [dir="rtl"] *){\n right: 0.375rem;\n}\n.rtl\\:right-\\[unset\\]:where([dir="rtl"], [dir="rtl"] *){\n right: unset;\n}\n.rtl\\:ml-2:where([dir="rtl"], [dir="rtl"] *){\n margin-left: 0.5rem;\n}\n.rtl\\:ml-\\[unset\\]:where([dir="rtl"], [dir="rtl"] *){\n margin-left: unset;\n}\n.rtl\\:mr-2:where([dir="rtl"], [dir="rtl"] *){\n margin-right: 0.5rem;\n}\n.rtl\\:mr-\\[unset\\]:where([dir="rtl"], [dir="rtl"] *){\n margin-right: unset;\n}\n.rtl\\:origin-top-right:where([dir="rtl"], [dir="rtl"] *){\n transform-origin: top right;\n}\n.rtl\\:-rotate-180:where([dir="rtl"], [dir="rtl"] *){\n --tw-rotate: -180deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.rtl\\:flex-row-reverse:where([dir="rtl"], [dir="rtl"] *){\n flex-direction: row-reverse;\n}\n.rtl\\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]){\n --tw-space-x-reverse: 1;\n}\n.rtl\\:pl-6:where([dir="rtl"], [dir="rtl"] *){\n padding-left: 1.5rem;\n}\n.rtl\\:pr-0:where([dir="rtl"], [dir="rtl"] *){\n padding-right: 0px;\n}\n.rtl\\:text-right:where([dir="rtl"], [dir="rtl"] *){\n text-align: right;\n}\n.rtl\\:after\\:ml-\\[unset\\]:where([dir="rtl"], [dir="rtl"] *)::after{\n content: var(--tw-content);\n margin-left: unset;\n}\n.rtl\\:after\\:mr-0\\.5:where([dir="rtl"], [dir="rtl"] *)::after{\n content: var(--tw-content);\n margin-right: 0.125rem;\n}\n.rtl\\:data-\\[focus-visible\\=true\\]\\:translate-x-3[data-focus-visible="true"]:where([dir="rtl"], [dir="rtl"] *){\n --tw-translate-x: 0.75rem;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.rtl\\:data-\\[hover\\=true\\]\\:translate-x-3[data-hover="true"]:where([dir="rtl"], [dir="rtl"] *){\n --tw-translate-x: 0.75rem;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.rtl\\:data-\\[open\\=true\\]\\:-rotate-90[data-open="true"]:where([dir="rtl"], [dir="rtl"] *){\n --tw-rotate: -90deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.group[data-selected="true"] .rtl\\:group-data-\\[selected\\=true\\]\\:ml-0:where([dir="rtl"], [dir="rtl"] *){\n margin-left: 0px;\n}\n.group[data-selected="true"] .rtl\\:group-data-\\[selected\\=true\\]\\:mr-4:where([dir="rtl"], [dir="rtl"] *){\n margin-right: 1rem;\n}\n.group[data-selected="true"] .rtl\\:group-data-\\[selected\\=true\\]\\:mr-5:where([dir="rtl"], [dir="rtl"] *){\n margin-right: 1.25rem;\n}\n.group[data-selected="true"] .rtl\\:group-data-\\[selected\\=true\\]\\:mr-6:where([dir="rtl"], [dir="rtl"] *){\n margin-right: 1.5rem;\n}\n.\\[\\&\\+\\.border-medium\\.border-danger\\]\\:ms-\\[calc\\(theme\\(borderWidth\\.medium\\)\\*-1\\)\\]+.border-medium.border-danger{\n margin-inline-start: calc(var(--nextui-border-width-medium) * -1);\n}\n.\\[\\&\\+\\.border-medium\\.border-default\\]\\:ms-\\[calc\\(theme\\(borderWidth\\.medium\\)\\*-1\\)\\]+.border-medium.border-default{\n margin-inline-start: calc(var(--nextui-border-width-medium) * -1);\n}\n.\\[\\&\\+\\.border-medium\\.border-primary\\]\\:ms-\\[calc\\(theme\\(borderWidth\\.medium\\)\\*-1\\)\\]+.border-medium.border-primary{\n margin-inline-start: calc(var(--nextui-border-width-medium) * -1);\n}\n.\\[\\&\\+\\.border-medium\\.border-secondary\\]\\:ms-\\[calc\\(theme\\(borderWidth\\.medium\\)\\*-1\\)\\]+.border-medium.border-secondary{\n margin-inline-start: calc(var(--nextui-border-width-medium) * -1);\n}\n.\\[\\&\\+\\.border-medium\\.border-success\\]\\:ms-\\[calc\\(theme\\(borderWidth\\.medium\\)\\*-1\\)\\]+.border-medium.border-success{\n margin-inline-start: calc(var(--nextui-border-width-medium) * -1);\n}\n.\\[\\&\\+\\.border-medium\\.border-warning\\]\\:ms-\\[calc\\(theme\\(borderWidth\\.medium\\)\\*-1\\)\\]+.border-medium.border-warning{\n margin-inline-start: calc(var(--nextui-border-width-medium) * -1);\n}\n.\\[\\&\\:not\\(\\:first-child\\)\\:not\\(\\:last-child\\)\\]\\:rounded-none:not(:first-child):not(:last-child){\n border-radius: 0px;\n}\n.\\[\\&\\:not\\(\\:first-child\\)\\]\\:-ml-1:not(:first-child){\n margin-left: -0.25rem;\n}\n.\\[\\&\\:not\\(\\:first-of-type\\)\\:not\\(\\:last-of-type\\)\\]\\:rounded-none:not(:first-of-type):not(:last-of-type){\n border-radius: 0px;\n}\n.\\[\\&\\:not\\(\\:first-of-type\\)\\]\\:ms-\\[calc\\(theme\\(borderWidth\\.2\\)\\*-1\\)\\]:not(:first-of-type){\n margin-inline-start: calc(2px * -1);\n}\n.\\[\\&\\>\\*\\]\\:relative>*{\n position: relative;\n}\n.\\[\\&\\>svg\\]\\:max-w-\\[theme\\(spacing\\.8\\)\\]>svg{\n max-width: 2rem;\n}\n.\\[\\&\\>tr\\]\\:first\\:rounded-lg:first-child>tr{\n border-radius: 0.5rem;\n}\n.\\[\\&\\>tr\\]\\:first\\:shadow-small:first-child>tr{\n --tw-shadow: var(--nextui-box-shadow-small);\n --tw-shadow-colored: var(--nextui-box-shadow-small);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.\\[\\&\\[data-hover\\=true\\]\\:not\\(\\[data-active\\=true\\]\\)\\]\\:bg-default-100[data-hover=true]:not([data-active=true]){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-100) / var(--nextui-default-100-opacity, var(--tw-bg-opacity)));\n}\n.\\[\\&\\[data-hover\\=true\\]\\:not\\(\\[data-active\\=true\\]\\)\\]\\:bg-default-200[data-hover=true]:not([data-active=true]){\n --tw-bg-opacity: 1;\n background-color: hsl(var(--nextui-default-200) / var(--nextui-default-200-opacity, var(--tw-bg-opacity)));\n}\n.\\[\\&_\\.chevron-icon\\]\\:flex-none .chevron-icon{\n flex: none;\n}\n.\\[\\&_\\.chevron-icon\\]\\:rotate-180 .chevron-icon{\n --tw-rotate: 180deg;\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.\\[\\&_\\.chevron-icon\\]\\:transition-transform .chevron-icon{\n transition-property: transform;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 250ms;\n}\n');M(":root {\n --chat-ui-background-color: #f5f5f5;\n --chat-ui-border-color: #d9d9d9;\n --chat-ui-text-color: #000;\n --chat-ui-assistant-background-color: #fff;\n --chat-ui-user-background-color: #daf1da;\n --chat-ui-input-textarea-bg: #fff;\n --chat-ui-input-textarea-color: #000;\n --chat-ui-input-textarea-placeholder-color: #aaa;\n --chat-ui-copy-button-background-color: #f6f8fa;\n --chat-ui-copy-button-text-color: rgba(60, 60, 60, 0.33);\n --chat-ui-copy-button-border-color: #dadde1;\n --chat-ui-input-section-menu-button-hover-bg: rgba(0, 123, 255, 0.1);\n --chat-ui-input-container-background-color: #fff;\n --chat-ui-input-border-radius: 10px;\n --chat-ui-input-container-border-radius: 6px;\n --chat-ui-message-bubble-border-radius: 10px;\n --chat-ui-message-bubble-font-size: 14px;\n --chat-ui-container-border-radius: 0px;\n --chat-ui-at-panel-bg: #f8f9fa;\n --chat-ui-at-panel-border: #e9ecef;\n --chat-ui-at-panel-border-radius: 8px;\n --chat-ui-at-panel-item-border-radius: 6px;\n --chat-ui-at-panel-padding: 8px;\n --chat-ui-at-panel-item-padding: 8px;\n --chat-ui-at-panel-item-gap: 4px;\n --chat-ui-at-panel-scrollbar-track: #f8f9fa;\n --chat-ui-at-panel-scrollbar-thumb: #ced4da;\n --chat-ui-at-panel-scrollbar-thumb-hover: #adb5bd;\n --chat-ui-at-panel-item-hover: #e9ecef;\n --chat-ui-at-panel-item-selected: #d0ebff;\n --chat-ui-suggestion-bubble-bg: #edf1f6;\n --chat-ui-suggestion-bubble-hover-bg: #e2e8f0;\n --chat-ui-suggestion-text-color: #4b5563;\n --chat-ui-suggestion-icon-color: #64748b;\n --chat-ui-copy-button-border-radius: 6px;\n --chat-ui-send-button-font-size: 16px;\n --chat-ui-send-button-height: 40px;\n --chat-ui-send-button-text-color: #fff;\n --chat-ui-send-button-border-radius: 50%;\n --chat-ui-send-button-width: 40px;\n --chat-ui-send-button-margin: 0 0 0 8px;\n --chat-ui-ref-tag-bg-color: #ccc;\n --chat-ui-ref-tag-active-bg-color: #0e639c;\n --chat-ui-ref-tag-active-color: #ffffff;\n --chat-ui-ref-close-button-color: #ffffff;\n --chat-ui-ref-expand-overlay-bg: linear-gradient(\n transparent,\n rgba(255, 255, 255, 0.8)\n );\n --chat-ui-ref-expand-overlay-hover-bg: linear-gradient(\n transparent,\n rgba(255, 255, 255, 0.9)\n );\n --chat-ui-ref-expand-icon-color: #333333;\n --chat-ui-ref-shadow-color: rgba(0, 0, 0, 0.1);\n --chat-ui-ref-bg-color: #1e1e1e;\n --chat-ui-ref-tag-bg-color: #2d2d2d;\n --chat-ui-ref-tag-color: #cccccc;\n --chat-ui-ref-tag-hover-bg-color: #4d4d4d;\n --chat-ui-ref-tag-active-bg-color: #4d4d4d;\n --chat-ui-ref-expand-overlay-bg: linear-gradient(\n transparent,\n rgba(30, 30, 30, 0.8)\n );\n --chat-ui-ref-expand-overlay-hover-bg: linear-gradient(\n transparent,\n rgba(30, 30, 30, 0.9)\n );\n --chat-ui-ref-expand-icon-color: #cccccc;\n --chat-ui-ref-shadow-color: rgba(0, 0, 0, 0.15);\n}\n\n.dark {\n --chat-ui-background-color: #1a1a1a;\n --chat-ui-border-color: #333;\n --chat-ui-text-color: #fff;\n --chat-ui-input-textarea-bg: #252525;\n --chat-ui-input-textarea-color: #fff;\n --chat-ui-input-textarea-placeholder-color: #888;\n --chat-ui-assistant-background-color: #333;\n --chat-ui-user-background-color: #333;\n --chat-ui-copy-button-background-color: #3c3c3c;\n --chat-ui-copy-button-text-color: #cccccc;\n --chat-ui-copy-button-border-color: #3c3c3c;\n --chat-ui-input-container-background-color: #1a1a1a;\n --chat-ui-at-panel-bg: #2c3e50;\n --chat-ui-at-panel-border: #34495e;\n --chat-ui-at-panel-scrollbar-track: #2c3e50;\n --chat-ui-at-panel-scrollbar-thumb: #7f8c8d;\n --chat-ui-at-panel-scrollbar-thumb-hover: #95a5a6;\n --chat-ui-at-panel-item-hover: #34495e;\n --chat-ui-at-panel-item-selected: #2c3e50;\n --chat-ui-send-button-background-color: #016fee;\n --chat-ui-suggestion-bubble-bg: #2c3e50;\n --chat-ui-suggestion-bubble-hover-bg: #4a5568;\n --chat-ui-suggestion-text-color: #bdc3c7;\n --chat-ui-suggestion-icon-color: #95a5a6;\n}\n\n.ai-app-chat-container {\n display: flex;\n flex-direction: column;\n min-height: 100%;\n background-color: var(--chat-ui-background-color);\n color: var(--chat-ui-text-color);\n transition: width 0.25s ease-in-out;\n}\n\n.ai-app-chat-messages {\n display: flex;\n flex-direction: column;\n flex: 1 1 auto;\n overflow-y: auto;\n margin: 0 auto;\n max-width: 800px;\n min-width: min(100% - 20px, 800px);\n width: calc(100% - 20px);\n margin-bottom: 10px;\n margin-top: 10px;\n}\n\n.ai-app-chat-input-container {\n position: sticky;\n bottom: 0;\n max-width: 800px;\n min-width: min(100% - 20px, 800px);\n width: calc(100% - 20px);\n display: flex;\n margin: 0 auto;\n margin-bottom: 10px;\n z-index: 10;\n transition: width 0.25s ease-in-out;\n}");import{useState as Ae,useEffect as Ne,useRef as Le,useCallback as Re}from"react";import De from"classnames";import{Spinner as Ee}from"@nextui-org/react";import{memo as Pe,useEffect as $e,useState as Oe}from"react";import Be from"classnames";import We from"react-markdown";import He from"remark-gfm";import Ue from"remark-math";import qe from"rehype-katex";import{Prism as Ve}from"react-syntax-highlighter";import{vscDarkPlus as Je}from"react-syntax-highlighter/dist/esm/styles/prism";import"katex/dist/katex.min.css";M('@charset "UTF-8";\n.dark .markdown-body {\n --text-primary: #f7fafc;\n --text-secondary: #cbd5e0;\n --bg-primary: none;\n --bg-secondary: #2d3748;\n --bg-quote: #2d3748;\n --border-quote: #4a5568;\n --link-color: #63b3ed;\n --link-hover: #90cdf4;\n --code-bg: #2d3748;\n --border-color: #4a5568;\n --heading-color: #f7fafc;\n --syntax-bg: #1e293b;\n --syntax-border: #334155;\n --syntax-highlight: rgba(56, 139, 253, 0.15);\n color: var(--text-primary);\n background-color: var(--bg-primary);\n}\n.dark .markdown-body pre {\n background: var(--syntax-bg) !important;\n border: 1px solid var(--syntax-border);\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);\n}\n\n.markdown-body {\n font-size: 16px;\n line-height: 1.6;\n font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\n letter-spacing: -0.011em;\n --text-primary: #1a202c;\n --text-secondary: #4a5568;\n --bg-primary: #ffffff;\n --bg-secondary: #f8fafc;\n --bg-quote: #f8fafc;\n --border-quote: #e2e8f0;\n --link-color: #3182ce;\n --link-hover: #2c5282;\n --code-bg: #f1f5f9;\n --border-color: #e2e8f0;\n --heading-color: #1a202c;\n --syntax-bg: #f8fafc;\n --syntax-border: #e2e8f0;\n --syntax-highlight: rgba(56, 139, 253, 0.15);\n color: var(--text-primary);\n background-color: var(--bg-primary);\n}\n.markdown-body h1,\n.markdown-body h2,\n.markdown-body h3,\n.markdown-body h4,\n.markdown-body h5,\n.markdown-body h6 {\n color: var(--heading-color);\n font-weight: 700;\n letter-spacing: -0.025em;\n margin: 2em 0 0.75em;\n line-height: 1.2;\n position: relative;\n}\n.markdown-body h1:first-child,\n.markdown-body h2:first-child,\n.markdown-body h3:first-child,\n.markdown-body h4:first-child,\n.markdown-body h5:first-child,\n.markdown-body h6:first-child {\n margin-top: 0.5em;\n}\n.markdown-body h1::before,\n.markdown-body h2::before,\n.markdown-body h3::before,\n.markdown-body h4::before,\n.markdown-body h5::before,\n.markdown-body h6::before {\n content: "";\n position: absolute;\n left: -0.8em;\n top: 0;\n bottom: 0;\n width: 3px;\n background: linear-gradient(to bottom, var(--link-color), transparent);\n border-radius: 3px;\n opacity: 0;\n transform: scaleY(0.7);\n transition: opacity 0.2s, transform 0.2s;\n}\n.markdown-body h1:hover::before,\n.markdown-body h2:hover::before,\n.markdown-body h3:hover::before,\n.markdown-body h4:hover::before,\n.markdown-body h5:hover::before,\n.markdown-body h6:hover::before {\n opacity: 1;\n transform: scaleY(1);\n}\n.markdown-body h1 {\n font-size: 2.25em;\n border-bottom: 1px solid var(--border-color);\n padding-bottom: 0.3em;\n margin-bottom: 1em;\n}\n.markdown-body h2 {\n font-size: 1.75em;\n border-bottom: 1px solid var(--border-color);\n padding-bottom: 0.2em;\n}\n.markdown-body h3 {\n font-size: 1.4em;\n}\n.markdown-body h4 {\n font-size: 1.2em;\n}\n.markdown-body h5 {\n font-size: 1em;\n}\n.markdown-body h6 {\n font-size: 0.9em;\n color: var(--text-secondary);\n}\n.markdown-body p {\n margin: 0 0 0.3em;\n line-height: 1.5;\n color: var(--text-secondary);\n}\n.markdown-body a {\n color: var(--link-color);\n text-decoration: none;\n transition: all 0.2s ease;\n border-bottom: 1px solid rgba(49, 130, 206, 0.2);\n padding-bottom: 1px;\n}\n.markdown-body a:hover {\n color: var(--link-hover);\n border-bottom-color: var(--link-color);\n}\n.markdown-body blockquote {\n margin: 1.75em 0;\n padding: 1em 1.5em;\n border-left: 4px solid var(--link-color);\n background: var(--bg-quote);\n border-radius: 0.5rem;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.markdown-body blockquote p {\n font-style: italic;\n}\n.markdown-body blockquote p:last-child {\n margin-bottom: 0;\n}\n.markdown-body blockquote cite {\n display: block;\n margin-top: 0.8em;\n font-size: 0.9em;\n text-align: right;\n color: var(--text-secondary);\n font-style: normal;\n}\n.markdown-body blockquote cite:before {\n content: "— ";\n}\n.markdown-body code:not([class*=language-]) {\n padding: 0.2em 0.4em;\n font-size: 0.875em;\n background: var(--code-bg);\n border-radius: 0.375rem;\n font-family: "SF Mono", Menlo, Monaco, Consolas, monospace;\n color: #e45649;\n}\n.markdown-body pre {\n margin: 1.75em 0;\n padding: 1.25em;\n border-radius: 0.5rem;\n background: var(--syntax-bg);\n overflow: auto;\n border: 1px solid var(--syntax-border);\n transition: all 0.3s ease;\n position: relative;\n}\n.markdown-body pre:hover {\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);\n}\n.markdown-body pre code {\n font-family: "SF Mono", Menlo, Monaco, Consolas, monospace;\n font-size: 0.9em;\n line-height: 1.6;\n color: var(--text-primary);\n display: block;\n padding: 0;\n overflow-x: auto;\n}\n.markdown-body pre code .token.comment,\n.markdown-body pre code .token.prolog,\n.markdown-body pre code .token.doctype,\n.markdown-body pre code .token.cdata {\n color: #8e96a3;\n}\n.markdown-body pre code .token.punctuation {\n color: #a8b2c0;\n}\n.markdown-body pre code .token.property,\n.markdown-body pre code .token.tag,\n.markdown-body pre code .token.constant,\n.markdown-body pre code .token.symbol {\n color: #e45649;\n}\n.markdown-body pre code .token.boolean,\n.markdown-body pre code .token.number {\n color: #986801;\n}\n.markdown-body pre code .token.selector,\n.markdown-body pre code .token.attr-name,\n.markdown-body pre code .token.string,\n.markdown-body pre code .token.char,\n.markdown-body pre code .token.builtin {\n color: #50a14f;\n}\n.markdown-body pre code .token.operator,\n.markdown-body pre code .token.entity,\n.markdown-body pre code .token.url,\n.markdown-body pre code .language-css .token.string,\n.markdown-body pre code .style .token.string {\n color: #0184bc;\n}\n.markdown-body pre code .token.atrule,\n.markdown-body pre code .token.attr-value,\n.markdown-body pre code .token.keyword {\n color: #a626a4;\n}\n.markdown-body pre code .token.function {\n color: #4078f2;\n}\n.markdown-body pre code .token.regex,\n.markdown-body pre code .token.important,\n.markdown-body pre code .token.variable {\n color: #e90;\n}\n.markdown-body pre code .token.important,\n.markdown-body pre code .token.bold {\n font-weight: bold;\n}\n.markdown-body pre code .token.italic {\n font-style: italic;\n}\n.markdown-body pre::before {\n content: attr(data-language);\n position: absolute;\n top: 0.5rem;\n right: 1rem;\n font-size: 0.7em;\n color: var(--text-secondary);\n opacity: 0.7;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n.markdown-body table {\n width: 100%;\n margin: 2em 0;\n border-collapse: separate;\n border-spacing: 0;\n border-radius: 0.5rem;\n overflow: hidden;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n.markdown-body table th,\n.markdown-body table td {\n border: 1px solid var(--border-color);\n padding: 0.85em 1.25em;\n}\n.markdown-body table th {\n background: var(--bg-secondary);\n font-weight: 600;\n text-align: left;\n}\n.markdown-body table thead tr {\n background: var(--bg-secondary);\n}\n.markdown-body table tbody tr {\n transition: background-color 0.2s ease;\n}\n.markdown-body table tbody tr:hover {\n background-color: var(--bg-secondary);\n}\n.markdown-body table tr:nth-child(even) {\n background: var(--bg-quote);\n}\n.markdown-body img {\n max-width: 100%;\n height: auto;\n border-radius: 0.5rem;\n margin: 1.75em 0;\n transition: all 0.3s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n}\n.markdown-body img:hover {\n transform: scale(1.01);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);\n}\n.markdown-body ul,\n.markdown-body ol {\n padding-left: 1.5em;\n margin: 1em 0;\n}\n.markdown-body ul li,\n.markdown-body ol li {\n position: relative;\n margin: 0.3em 0;\n padding-left: 0.5em;\n color: var(--text-secondary);\n}\n.markdown-body ul {\n list-style-type: disc;\n}\n.markdown-body ul ul {\n list-style-type: circle;\n margin: 0.3em 0 0.3em 0.5em;\n}\n.markdown-body ul ul ul {\n list-style-type: square;\n}\n.markdown-body ol {\n list-style-type: decimal;\n padding-left: 1em;\n}\n.markdown-body ol li {\n padding-left: 0.2em;\n margin-bottom: 0.5em;\n}\n.markdown-body ol li::marker {\n margin-top: 0.2em;\n}\n.markdown-body ol ol {\n list-style-type: lower-alpha;\n margin: 0.5em 0 0.3em 0.5em;\n}\n.markdown-body ol ol ol {\n list-style-type: lower-roman;\n}\n.markdown-body ul li::before,\n.markdown-body ul li:hover::before,\n.markdown-body ol li::before,\n.markdown-body ol li:hover::before {\n display: none;\n}\n.markdown-body hr {\n margin: 2.5em 0;\n border: none;\n height: 1px;\n background: linear-gradient(to right, transparent, var(--border-color), transparent);\n}\n.markdown-body dl {\n margin: 1.5em 0;\n}\n.markdown-body dl dt {\n font-weight: 600;\n color: var(--heading-color);\n margin-top: 1em;\n}\n.markdown-body dl dd {\n margin-left: 1.5em;\n color: var(--text-secondary);\n}\n.markdown-body kbd {\n display: inline-block;\n padding: 0.2em 0.4em;\n font-size: 0.85em;\n font-family: "SF Mono", Menlo, Monaco, Consolas, monospace;\n line-height: 1;\n color: var(--text-primary);\n background-color: var(--bg-secondary);\n border: 1px solid var(--border-color);\n border-radius: 0.25em;\n box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);\n white-space: nowrap;\n}\n.markdown-body mark {\n background-color: rgba(255, 220, 0, 0.2);\n padding: 0.1em 0.2em;\n border-radius: 0.2em;\n}\n.markdown-body .footnote {\n font-size: 0.85em;\n vertical-align: super;\n color: var(--link-color);\n}\n.markdown-body .footnotes {\n margin-top: 3em;\n border-top: 1px solid var(--border-color);\n padding-top: 1em;\n}\n.markdown-body .footnotes ol {\n font-size: 0.9em;\n color: var(--text-secondary);\n}\n.markdown-body .footnotes .footnote-backref {\n margin-left: 0.5em;\n}');import{jsx as Ke}from"react/jsx-runtime";var Ge=({content:t,isDark:e=!1})=>Ke("div",{className:"markdown-body "+(e?"dark":""),children:Ke(We,{remarkPlugins:[He,Ue],rehypePlugins:[qe],components:{code(t){var e=t,{node:n,className:a,children:r}=e,o=d(e,["node","className","children"]);const i=/language-(\w+)/.exec(a||"");return i?Ke(Ve,{language:i[1],style:Je,children:String(r).replace(/\n$/,"")}):Ke("code",s(c({className:a},o),{children:r}))}},children:t})});import{jsx as Ze,jsxs as Qe}from"react/jsx-runtime";var tn=Pe((function({loading:t,message:e,customMessageRender:n,className:a,isDark:r=!1,showAnimation:o=!1,slots:i={}}){var c;const[s,d]=Oe(!1);if($e((()=>{if(null==e?void 0:e.isDeleting){d(!0);const t=setTimeout((()=>{d(!1)}),250);return console.log(e),()=>clearTimeout(t)}}),[e]),t||!e)return Ze("div",{className:Be(a),children:null!=(c=i.customLoading)?c:Ze(Ee,{size:"sm"})});if(n){const t=n(e);if(t)return Ze("div",{className:Be("assistant"===e.role?"is-assistant":"is-user",s?"fade-out":"",a),children:t})}const{showCopyButton:l="plain-text"===e.type&&"assistant"===e.role}=e;return Ze("div",{className:Be("assistant"===e.role?"is-assistant":"is-user",s?"fade-out":"",o?"animate-in":"",a),children:(()=>{var t;const n=[];switch(e.type){case"plain-text":n.push(Ze(Ge,{content:String(e.content||""),isDark:r,smooth:!e.isFinal&&"assistant"===e.role},"plain-text"));break;case"file":n.push(Ze(Pt,{file:e.content,showDeleteIcon:!1},"file"));break;default:return null}return(i.renderCustomMessageActionUI||l)&&n.push(Qe("div",{className:"flex gap-2 items-center mt-2 w-full",children:[null==(t=i.renderCustomMessageActionUI)?void 0:t.call(i,e),l?Ze(N,{getCopyText:()=>e.content}):null]},"copy-button")),n})()})}));import{useCallback as en}from"react";import{IoArrowForward as nn}from"react-icons/io5";M(".ai-app-suggestion-bubble {\n display: flex;\n align-items: center;\n justify-content: space-between;\n max-width: 300px;\n width: 100%;\n margin-bottom: 6px;\n padding: 8px 12px;\n background-color: var(--chat-ui-assistant-background-color);\n border-radius: var(--chat-ui-message-bubble-border-radius);\n transition: background-color 0.3s ease;\n cursor: pointer;\n}\n.ai-app-suggestion-bubble:hover {\n background-color: var(--chat-ui-suggestion-bubble-hover-bg);\n}\n\n.ai-app-suggestion-text {\n font-size: 14px;\n color: var(--chat-ui-suggestion-text-color);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n margin-right: 8px;\n}\n\n.ai-app-suggestion-icon {\n flex-shrink: 0;\n color: var(--chat-ui-suggestion-icon-color);\n font-size: 18px;\n}");import{jsx as an,jsxs as rn}from"react/jsx-runtime";var on=({content:t,setInputText:e,onClick:n})=>{const a=en((()=>{e(t),null==n||n(t)}),[t,n,e]);return rn("div",{className:"ai-app-suggestion-bubble",onClick:a,children:[an("span",{className:"ai-app-suggestion-text",children:t}),an(nn,{className:"ai-app-suggestion-icon"})]})};import{Avatar as cn,AvatarIcon as sn}from"@nextui-org/react";import{FaRobot as dn}from"react-icons/fa";M(".ai-app-message-bubble {\n display: flex;\n align-items: start;\n font-size: var(--chat-ui-message-bubble-font-size);\n flex-direction: row;\n word-break: break-all;\n white-space: normal;\n font-weight: 400;\n min-width: 100%;\n gap: 8px;\n width: 0;\n flex: 1 1;\n padding: 4px 10px;\n margin-bottom: 10px;\n border-radius: var(--chat-ui-message-bubble-border-radius);\n position: relative;\n}\n@keyframes fadeInLeft {\n from {\n transform: translateX(-30px);\n opacity: 0;\n }\n to {\n transform: translateX(0);\n opacity: 1;\n }\n}\n.ai-app-message-bubble.fade-out {\n animation: fadeOutLeft 0.25s ease-out forwards;\n}\n@keyframes fadeOutLeft {\n from {\n transform: translateX(0);\n opacity: 1;\n }\n to {\n transform: translateX(-30px);\n opacity: 0;\n }\n}\n.ai-app-message-bubble.animate-in {\n animation: fadeInLeft 0.5s ease-out;\n}");import{jsx as ln,jsxs as un}from"react/jsx-runtime";var pn=t=>{const{messages:e,messageLoading:n,welcomeMessage:a,onMessageSend:r,slots:o,customClassNames:i,customStyles:c,customMessageRender:s,messageEndRef:d,isDark:l,setInputText:u,suggestions:p,onSuggestionClick:w,avatar:g={user:!0},autoScrollToBottom:v=!1,isUserScrollingRef:x}=t,[b,y]=Ae(15),[h,f]=Ae(!0),m=Le(null),k=Le(null),z=Le(null),_=Le(null),X=ln(dn,{width:22,height:22,size:22,style:{marginTop:8,marginLeft:4,color:"#4A90E2"}}),Y=ln("div",{className:"flex justify-center items-center",style:{width:22,height:22,borderRadius:"50%",marginTop:"1px"},children:ln(sn,{})}),S=Re((()=>{const t=_.current,n=(null==t?void 0:t.scrollHeight)||0,a=(null==t?void 0:t.scrollTop)||0;y((t=>Math.min(t+15,e.length))),requestAnimationFrame((()=>{if(t){const e=t.scrollHeight;h?(t.scrollTop=e,f(!1)):t.scrollTop=a+(e-n)}}))}),[e.length,h]),j=Re((()=>{y(15)}),[]);Ne((()=>{const t={root:_.current,rootMargin:"400px 0px 0px 0px",threshold:0};return m.current=new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting&&(t.target===k.current?S():t.target===z.current&&j())}))}),t),k.current&&m.current.observe(k.current),z.current&&m.current.observe(z.current),()=>{m.current&&m.current.disconnect()}}),[S,j]);const C=e.slice(-b).filter(Boolean);Ne((()=>{d.current&&v&&d.current.scrollIntoView({behavior:"smooth"})}),[]),Ne((()=>{const t=_.current;if(!t)return;const e=()=>{const e=Math.abs(t.scrollHeight-t.scrollTop-t.clientHeight)<1;x.current=!e};return t.addEventListener("scroll",e),()=>{t.removeEventListener("scroll",e)}}),[x]);const F=(t,e)=>e?ln(cn,{src:e,radius:"sm",style:{width:"20px",height:"20px"}}):"assistant"===t?(null==g?void 0:g.assistant)?!0===g.assistant?X:g.assistant:null:"user"===t?(null==g?void 0:g.user)?!0===g.user?Y:g.user:null:void 0;return ln("div",{ref:_,className:De("ai-app-chat-messages",i.messageList),style:c.messageList,children:un("div",{style:{maxWidth:"800px",minWidth:"min(100%, 800px)",margin:"0 auto"},children:[ln("div",{ref:k}),a?un("div",{className:"ai-app-message-bubble mb-2",children:[ln("div",{className:"relative",children:F("assistant",a.avatar)}),ln(tn,{message:a,customMessageRender:s,setInputText:u,slots:o})]}):null,C.map(((t,n)=>un("div",{style:c.message,children:[o.beforeMessage,un("div",{className:"min-w-full ai-app-message-bubble",children:[F(t.role,t.avatar),ln(tn,{message:t,sendMessage:r,className:`w-0 flex-1 ${i.message}`,customMessageRender:s,isDark:l,setInputText:u,slots:o,showAnimation:e.findIndex((e=>e===t))>=C.length-15})]}),o.afterMessage]},`${t.role}-${t.timestamp||JSON.stringify(t.content)}_${n}`))),n?ln(tn,{className:"mx-2",loading:!0,setInputText:u,slots:o}):null,null==p?void 0:p.map((t=>ln(on,{content:t,setInputText:u,onClick:w},t))),ln("div",{ref:d}),ln("div",{ref:z})]})})};import{jsx as wn,jsxs as gn}from"react/jsx-runtime";var vn=je(Fe(((t,e)=>{const{states:n=x,onMessageSend:a,onMessageAbort:r,styles:o,classNames:i={},storageDbName:s,customMessageStorage:d,disableInput:l=!1,slots:u={},conversationId:p="default",features:g,customMessageRender:v,welcomeMessage:b,commandPanelConfig:y,inputPlaceholder:h,onCommandTrigger:f,onCommandSelect:m,onCommandDelete:k,isDark:_,onSuggestionClick:X,onClearConversationHistory:Y,suggestions:S=[],avatar:j}=t,C=Se((()=>d||(s?new w(s):void 0)),[d,s]),{messages:F,messageLoading:M,clearMessages:I,addMessage:T,setInputText:A}=z({states:n,conversationId:p,customMessageStorage:C()}),[N]=Te(n.messageEndRefAtom),[L]=Te(n.isUserScrollingRefAtom),R=Ce(null),D=Ce(null);Me(e,(()=>({triggerCommand:t=>{var e;null==(e=D.current)||e.triggerCommand(t)},focusInput:()=>{var t;null==(t=D.current)||t.focus()},getInputTextArea:()=>{var t;return null==(t=D.current)?void 0:t.getInputTextArea()}})));const E=Ye((()=>({container:null==o?void 0:o.container,message:null==o?void 0:o.message,input:null==o?void 0:o.input,inputContainer:null==o?void 0:o.inputContainer,messageList:null==o?void 0:o.messageList})),[o]),P=Ye((()=>({container:i.container,message:i.message,input:i.input,inputContainer:i.inputContainer,messageList:i.messageList})),[i]),$=Se((t=>null==v?void 0:v(t)),[v]),O=Ye((()=>F.filter((t=>"user"===t.role)).filter((t=>"plain-text"===t.type))),[F.length]);return gn("div",{className:Ie("ai-app-chat-container",P.container),ref:R,style:c({},E.container),children:[u.beforeMessageList,wn(pn,{messages:F,messageLoading:M,welcomeMessage:b,onMessageSend:a,slots:u,customClassNames:P,customStyles:E,customMessageRender:$,messageEndRef:N,isDark:_,setInputText:A,suggestions:S,avatar:j,onSuggestionClick:X,isUserScrollingRef:L}),u.afterMessageList,u.beforeInputContainer,wn("div",{className:Ie("ai-app-chat-input-container",P.inputContainer),style:E.inputContainer,children:wn(Xe,{ref:D,disableInput:l,userMessages:O,sendMessage:a,addMessage:T,slots:u,style:E.input,features:g,clearMessages:I,chatStates:n,commandPanelConfig:y,inputPlaceholder:h,abortMessage:r,onCommandTrigger:f,onCommandSelect:m,onCommandDelete:k,onClearConversationHistory:Y})}),u.afterInputContainer]})})));export{vn as ChatUI,ae as CommandPanel,N as CopyButton,w as DefaultMessageStorage,ie as FigmaIcon,Pt as FileItemPreview,$t as FilePreview,m as InputFileType,Ge as MarkdownRenderer,h as MessageRole,f as MessageType,v as createChatStateAtoms,gt as getFileIcon,pt as getFileType,zt as handleFileInput,kt as handleImage,bt as normalizeFiles,xt as normalizeFilesPrompt,Bt as parseCommandRules,z as useChat};
diff --git a/apps/agent-tars/src/vendor/chat-ui/package.json b/apps/agent-tars/src/vendor/chat-ui/package.json
new file mode 100644
index 000000000..ff2e69c2e
--- /dev/null
+++ b/apps/agent-tars/src/vendor/chat-ui/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "chat-ui",
+ "module": "./index.js",
+ "main": "./index.js",
+ "types": "./index.d.ts"
+}
diff --git a/apps/agent-tars/static/agent-tars-demo-01.mp4 b/apps/agent-tars/static/agent-tars-demo-01.mp4
new file mode 100644
index 000000000..b6b9b5989
Binary files /dev/null and b/apps/agent-tars/static/agent-tars-demo-01.mp4 differ
diff --git a/apps/agent-tars/static/dmg-background.png b/apps/agent-tars/static/dmg-background.png
new file mode 100644
index 000000000..21e9c9e82
Binary files /dev/null and b/apps/agent-tars/static/dmg-background.png differ
diff --git a/apps/agent-tars/static/dmg-background@2x.png b/apps/agent-tars/static/dmg-background@2x.png
new file mode 100644
index 000000000..6c79aeef7
Binary files /dev/null and b/apps/agent-tars/static/dmg-background@2x.png differ
diff --git a/apps/agent-tars/static/hero.png b/apps/agent-tars/static/hero.png
new file mode 100644
index 000000000..1b2e08b7e
Binary files /dev/null and b/apps/agent-tars/static/hero.png differ
diff --git a/apps/agent-tars/tailwind.config.ts b/apps/agent-tars/tailwind.config.ts
new file mode 100644
index 000000000..c7979f504
--- /dev/null
+++ b/apps/agent-tars/tailwind.config.ts
@@ -0,0 +1,36 @@
+import resolveConfig from 'tailwindcss/resolveConfig';
+
+export default resolveConfig({
+ content: ['./src/renderer/src/**/*.{html,ts,tsx,jsx,js}'],
+ theme: {
+ extend: {
+ animation: {
+ shine: 'shine 2s linear infinite',
+ pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
+ glow: 'glow 2s ease-in-out infinite alternate',
+ },
+ keyframes: {
+ shine: {
+ '0%': { transform: 'translateX(-100%)' },
+ '100%': { transform: 'translateX(100%)' },
+ },
+ glow: {
+ '0%': {
+ opacity: '0.5',
+ transform: 'translateX(-10%) translateY(-10%)',
+ },
+ '100%': {
+ opacity: '0.7',
+ transform: 'translateX(10%) translateY(10%)',
+ },
+ },
+ },
+ backdropFilter: {
+ none: 'none',
+ blur: 'blur(20px)',
+ },
+ },
+ },
+ darkMode: 'class',
+ plugins: [],
+});
diff --git a/apps/agent-tars/tsconfig.base.json b/apps/agent-tars/tsconfig.base.json
new file mode 100644
index 000000000..9aaf3f00a
--- /dev/null
+++ b/apps/agent-tars/tsconfig.base.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "moduleResolution": "bundler",
+ "paths": {
+ "@shared/*": ["./src/shared/*"],
+ "@main/*": ["./src/main/*"],
+ "@renderer/*": ["./src/renderer/src/*"],
+ "@resources/*": ["./resources/*"],
+ "@vendor/chat-ui": ["./src/vendor/chat-ui"]
+ }
+ }
+}
diff --git a/apps/agent-tars/tsconfig.json b/apps/agent-tars/tsconfig.json
new file mode 100644
index 000000000..d5b8e7465
--- /dev/null
+++ b/apps/agent-tars/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.node.json" },
+ { "path": "./tsconfig.web.json" }
+ ],
+ "compilerOptions": {
+ "target": "ES2020",
+ "resolveJsonModule": true,
+ "esModuleInterop": true
+ }
+}
diff --git a/apps/agent-tars/tsconfig.node.json b/apps/agent-tars/tsconfig.node.json
new file mode 100644
index 000000000..f29a1317e
--- /dev/null
+++ b/apps/agent-tars/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "extends": ["@electron-toolkit/tsconfig/tsconfig.node.json", "./tsconfig.base.json"],
+ "include": ["electron.vite.config.*", "src/main/**/*", "forge.config.ts", "src/preload/**/*", "app/electron.vite.config.ts"],
+ "compilerOptions": {
+ "composite": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "types": ["electron-vite/node"]
+ }
+}
diff --git a/apps/agent-tars/tsconfig.web.json b/apps/agent-tars/tsconfig.web.json
new file mode 100644
index 000000000..19ca3f9fd
--- /dev/null
+++ b/apps/agent-tars/tsconfig.web.json
@@ -0,0 +1,16 @@
+{
+ "extends": ["@electron-toolkit/tsconfig/tsconfig.web.json", "./tsconfig.base.json"],
+ "include": [
+ "src/renderer/src/env.d.ts",
+ "src/renderer/src/**/*.ts",
+ "src/renderer/src/**/*.tsx",
+
+ "src/main/ipcRoutes/index.ts",
+ "src/preload/*.d.ts",
+ ],
+ "compilerOptions": {
+ "composite": true,
+ "jsx": "react-jsx",
+ "baseUrl": "."
+ }
+}
diff --git a/apps/agent-tars/vite.config.ts b/apps/agent-tars/vite.config.ts
new file mode 100644
index 000000000..4bf7548b1
--- /dev/null
+++ b/apps/agent-tars/vite.config.ts
@@ -0,0 +1,56 @@
+import path, { resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { viteSingleFile } from 'vite-plugin-singlefile';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export default defineConfig(
+ (async () => {
+ const { default: tsconfigPaths } = await import('vite-tsconfig-paths');
+ return {
+ worker: false,
+ root: 'src/renderer',
+ resolve: {
+ alias: {
+ '@ui-tars/electron-ipc/renderer': path.resolve(
+ __dirname,
+ './src/renderer/mock/ipc.ts',
+ ),
+ },
+ },
+ define: {
+ 'process.env.REPORT_HTML_MODE': 'true',
+ },
+ build: {
+ outDir: resolve(__dirname, './dist/reporter'),
+ rollupOptions: {
+ input: {
+ main: resolve('./src/renderer/index.html'),
+ },
+ output: {
+ manualChunks: undefined,
+ inlineDynamicImports: true,
+ format: 'iife',
+ entryFileNames: '[name].js',
+ chunkFileNames: '[name].js',
+ assetFileNames: '[name][extname]',
+ },
+ },
+ cssCodeSplit: false,
+ assetsInlineLimit: 100000000,
+ minify: true,
+ },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ api: 'modern',
+ },
+ },
+ },
+
+ plugins: [react(), tsconfigPaths(), viteSingleFile()],
+ };
+ })(),
+);
diff --git a/apps/agent-tars/vitest.config.ts b/apps/agent-tars/vitest.config.ts
new file mode 100644
index 000000000..35d1b79ef
--- /dev/null
+++ b/apps/agent-tars/vitest.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vitest/config';
+import path from 'path';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ },
+ resolve: {
+ alias: {
+ '@renderer': path.resolve(__dirname, 'src/renderer/src'),
+ '@vendor': path.resolve(__dirname, 'src/renderer/vendor'),
+ },
+ },
+});
diff --git a/apps/ui-tars/e2e/app.test.ts b/apps/ui-tars/e2e/app.test.ts
index f0b171918..5d72c9e5d 100644
--- a/apps/ui-tars/e2e/app.test.ts
+++ b/apps/ui-tars/e2e/app.test.ts
@@ -52,6 +52,8 @@ test('app can launch', async () => {
test.setTimeout(60_000);
await page.waitForLoadState('domcontentloaded', { timeout: 0 });
+ await page.waitForSelector('button', { state: 'visible' });
+
const buttonElement = await page.$('button');
expect(await buttonElement?.isVisible()).toBe(true);
});
diff --git a/apps/ui-tars/e2e/vitest.config.mts b/apps/ui-tars/e2e/vitest.config.mts
index e28d01b3c..4add3cb7d 100644
--- a/apps/ui-tars/e2e/vitest.config.mts
+++ b/apps/ui-tars/e2e/vitest.config.mts
@@ -21,6 +21,7 @@ export default defineProject({
},
plugins: [
+ // @ts-ignore
tsconfigPath({
projects: ['../tsconfig.node.json'],
}),
diff --git a/apps/ui-tars/forge.config.ts b/apps/ui-tars/forge.config.ts
index a5f5159a9..44537fe11 100644
--- a/apps/ui-tars/forge.config.ts
+++ b/apps/ui-tars/forge.config.ts
@@ -34,7 +34,7 @@ const ignorePattern = new RegExp(
const unpack = `**/node_modules/{@img,${[...keepModules].join(',')}}/**/*`;
console.log('keepModules', Object.keys(pkg.dependencies));
-console.log('needSubDependencies', Object.keys(needSubDependencies));
+console.log('needSubDependencies', needSubDependencies);
const keepLanguages = new Set(['en', 'en_GB', 'en-US', 'en_US']);
const noopAfterCopy = (
_buildPath,
@@ -192,6 +192,7 @@ const config: ForgeConfig = {
repository: { owner: 'bytedance', name: 'ui-tars-desktop' },
draft: true,
force: true,
+ generateReleaseNotes: true,
},
},
],
diff --git a/apps/ui-tars/package.json b/apps/ui-tars/package.json
index 2b3ded091..38f73f327 100644
--- a/apps/ui-tars/package.json
+++ b/apps/ui-tars/package.json
@@ -23,7 +23,7 @@
"test": "vitest",
"test:bench": "vitest bench",
"coverage": "vitest run --coverage",
- "pretest:e2e": "npm run build:dist && cross-env CI=e2e npm run package",
+ "build:e2e": "npm run build:dist && cross-env CI=e2e npm run package",
"test:e2e": "playwright test",
"build:deps": "pnpm --filter \"!ui-tars-desktop,ui-tars-desktop...\" build && cd packages/visualizer && pnpm install --ignore-workspace",
"build:dist": "cross-env NODE_ENV=production electron-vite build",
@@ -97,12 +97,12 @@
"@types/node": "^20.14.8",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
- "@vitejs/plugin-react": "^4.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-istanbul": "^3.0.3",
"cross-env": "^7.0.3",
"electron": "34.1.1",
"electron-packager-languages": "0.5.0",
- "electron-vite": "^2.3.0",
+ "electron-vite": "^3.0.0",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.25.0",
"eslint-plugin-react": "^7.34.3",
@@ -112,9 +112,9 @@
"ts-node": "^10.9.2",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
- "vite": "^5.3.1",
+ "vite": "^6.1.0",
"vite-tsconfig-paths": "^5.1.4",
- "vitest": "^3.0.2"
+ "vitest": "^3.0.8"
},
"engines": {
"node": ">=20.x"
diff --git a/apps/ui-tars/src/main/vitest.config.mts b/apps/ui-tars/src/main/vitest.config.mts
index 1b73d9704..4310f4f72 100644
--- a/apps/ui-tars/src/main/vitest.config.mts
+++ b/apps/ui-tars/src/main/vitest.config.mts
@@ -20,6 +20,7 @@ export default defineProject({
},
plugins: [
+ // @ts-ignore
tsconfigPath({
projects: ['../../tsconfig.node.json'],
}),
diff --git a/apps/ui-tars/vitest.config.mts b/apps/ui-tars/vitest.config.mts
index 3eda07bf6..6006ff3bd 100644
--- a/apps/ui-tars/vitest.config.mts
+++ b/apps/ui-tars/vitest.config.mts
@@ -19,6 +19,7 @@ export default defineProject({
},
plugins: [
+ // @ts-ignore
tsconfigPath({
projects: ['./tsconfig.node.json'],
}),
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 000000000..8da10fabe
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,60 @@
+# Deployment
+
+### ⚠️ Important Announcement: GGUF Model Performance
+
+The **GGUF model** has undergone quantization, but unfortunately, its performance cannot be guaranteed. As a result, we have decided to **downgrade** it.
+
+💡 **Alternative Solution**:
+You can use **[Cloud Deployment](#cloud-deployment)** or **[Local Deployment [vLLM]](#local-deployment-vllm)**(If you have enough GPU resources) instead.
+
+We appreciate your understanding and patience as we work to ensure the best possible experience.
+
+## Cloud Deployment
+
+We recommend using HuggingFace Inference Endpoints for fast deployment.
+We provide two docs for users to refer:
+
+English version: [GUI Model Deployment Guide](https://juniper-switch-f10.notion.site/GUI-Model-Deployment-Guide-17b5350241e280058e98cea60317de71)
+
+中文版: [GUI模型部署教程](https://bytedance.sg.larkoffice.com/docx/TCcudYwyIox5vyxiSDLlgIsTgWf#U94rdCxzBoJMLex38NPlHL21gNb)
+
+## Local Deployment [vLLM]
+We recommend using vLLM for fast deployment and inference. You need to use `vllm>=0.6.1`.
+```bash
+pip install -U transformers
+VLLM_VERSION=0.6.6
+CUDA_VERSION=cu124
+pip install vllm==${VLLM_VERSION} --extra-index-url https://download.pytorch.org/whl/${CUDA_VERSION}
+
+```
+### Download the Model
+We provide three model sizes on Hugging Face: **2B**, **7B**, and **72B**. To achieve the best performance, we recommend using the **7B-DPO** or **72B-DPO** model (based on your hardware configuration):
+
+- [2B-SFT](https://huggingface.co/bytedance-research/UI-TARS-2B-SFT)
+- [7B-SFT](https://huggingface.co/bytedance-research/UI-TARS-7B-SFT)
+- [7B-DPO](https://huggingface.co/bytedance-research/UI-TARS-7B-DPO)
+- [72B-SFT](https://huggingface.co/bytedance-research/UI-TARS-72B-SFT)
+- [72B-DPO](https://huggingface.co/bytedance-research/UI-TARS-72B-DPO)
+
+
+### Start an OpenAI API Service
+Run the command below to start an OpenAI-compatible API service:
+
+```bash
+python -m vllm.entrypoints.openai.api_server --served-model-name ui-tars --model
+```
+
+### Input your API information
+
+
+
+
+
+> **Note**: VLM Base Url is OpenAI compatible API endpoints (see [OpenAI API protocol document](https://platform.openai.com/docs/guides/vision/uploading-base-64-encoded-images) for more details).
diff --git a/docs/quick-start.md b/docs/quick-start.md
new file mode 100644
index 000000000..447b783a8
--- /dev/null
+++ b/docs/quick-start.md
@@ -0,0 +1,33 @@
+# Quick Start
+
+## Download
+
+You can download the [latest release](https://github.com/bytedance/UI-TARS-desktop/releases/latest) version of UI-TARS Desktop from our releases page.
+
+> **Note**: If you have [Homebrew](https://brew.sh/) installed, you can install UI-TARS Desktop by running the following command:
+> ```bash
+> brew install --cask ui-tars
+> ```
+
+## Install
+
+### MacOS
+
+1. Drag **UI TARS** application into the **Applications** folder
+
+
+2. Enable the permission of **UI TARS** in MacOS:
+ - System Settings -> Privacy & Security -> **Accessibility**
+ - System Settings -> Privacy & Security -> **Screen Recording**
+
+
+3. Then open **UI TARS** application, you can see the following interface:
+
+
+
+### Windows
+
+**Still to run** the application, you can see the following interface:
+
+
+
diff --git a/docs/sdk.md b/docs/sdk.md
index 4e55871e9..209a75f46 100644
--- a/docs/sdk.md
+++ b/docs/sdk.md
@@ -1,4 +1,4 @@
-# @ui-tars/sdk Guide(Beta)
+# @ui-tars/sdk Guide (Experimental)
## Overview
diff --git a/examples/operator-browserbase/eslint.config.mjs b/examples/operator-browserbase/eslint.config.mjs
index c85fb67c4..7f86eca7f 100644
--- a/examples/operator-browserbase/eslint.config.mjs
+++ b/examples/operator-browserbase/eslint.config.mjs
@@ -1,6 +1,6 @@
-import { dirname } from "path";
-import { fileURLToPath } from "url";
-import { FlatCompat } from "@eslint/eslintrc";
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -10,7 +10,7 @@ const compat = new FlatCompat({
});
const eslintConfig = [
- ...compat.extends("next/core-web-vitals", "next/typescript"),
+ ...compat.extends('next/core-web-vitals', 'next/typescript'),
];
export default eslintConfig;
diff --git a/package.json b/package.json
index 0cf3c586a..3e88cceb1 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,9 @@
"author": "ByteDance",
"scripts": {
"commit": "oco",
+ "bootstrap": "pnpm i",
+ "dev:ui-tars": "turbo run ui-tars-desktop#dev",
+ "dev:agent-tars": "turbo run agent-tars-app#dev",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"test": "vitest",
@@ -18,6 +21,7 @@
"prepare": "husky"
},
"devDependencies": {
+ "turbo": "^2.4.4",
"@changesets/cli": "^2.27.11",
"@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0",
diff --git a/packages/agent-infra/browser-use/.vscode/launch.json b/packages/agent-infra/browser-use/.vscode/launch.json
new file mode 100644
index 000000000..e9a5f0fbb
--- /dev/null
+++ b/packages/agent-infra/browser-use/.vscode/launch.json
@@ -0,0 +1,34 @@
+{
+ "configurations": [
+ {
+ "name": "test.ts",
+ "type": "node",
+ "request": "launch",
+
+ // Debug current file in VSCode
+ "program": "${workspaceFolder}/test/test.ts",
+
+ /*
+ * Path to tsx binary
+ * Assuming locally installed
+ */
+ "runtimeExecutable": "tsx",
+
+ /*
+ * Open terminal when debugging starts (Optional)
+ * Useful to see console.logs
+ */
+ "console": "integratedTerminal",
+ "internalConsoleOptions": "neverOpen",
+
+ // Files to exclude from debugger (e.g. call stack)
+ "skipFiles": [
+ // Node.js internal core modules
+ "/**",
+
+ // Ignore all dependencies (optional)
+ "${workspaceFolder}/node_modules/**"
+ ]
+ }
+ ]
+}
diff --git a/packages/agent-infra/browser-use/README.md b/packages/agent-infra/browser-use/README.md
new file mode 100644
index 000000000..946a30264
--- /dev/null
+++ b/packages/agent-infra/browser-use/README.md
@@ -0,0 +1,11 @@
+# @agent-infra/browser-use
+
+A browser automation and interaction library for AI agents, providing structured DOM access and browser control capabilities.
+
+## Credits
+
+Thanks to:
+
+- The [browser-use](https://github.com/browser-use/browser-use) project which helps us operate the browser better.
+- [alexchenzl](https://github.com/alexchenzl) for creating a great [nanobrowser](https://github.com/nanobrowser/nanobrowser) Chrome extension from which we got a lot of technical references when implementing browser in Electron
+- The [puppeteer](https://github.com/puppeteer/puppeteer) project which helps us operate the browser better.
diff --git a/packages/agent-infra/browser-use/assets/buildDomTree.js b/packages/agent-infra/browser-use/assets/buildDomTree.js
new file mode 100644
index 000000000..45d19a13b
--- /dev/null
+++ b/packages/agent-infra/browser-use/assets/buildDomTree.js
@@ -0,0 +1,735 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/public/buildDomTree.js
+ * https://github.com/browser-use/browser-use/blob/main/browser_use/dom/buildDomTree.js
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ *
+ * MIT License
+ * Copyright (c) 2024 Gregor Zunic
+ * https://github.com/browser-use/browser-use/blob/main/LICENSE
+ */
+window.buildDomTree = (
+ args = {
+ doHighlightElements: true,
+ focusHighlightIndex: -1,
+ viewportExpansion: 0,
+ },
+) => {
+ const { doHighlightElements, focusHighlightIndex, viewportExpansion } = args;
+ let highlightIndex = 0; // Reset highlight index
+
+ // Quick check to confirm the script receives focusHighlightIndex
+ console.log('focusHighlightIndex:', focusHighlightIndex);
+
+ function highlightElement(element, index, parentIframe = null) {
+ // Create or get highlight container
+ let container = document.getElementById('playwright-highlight-container');
+ if (!container) {
+ container = document.createElement('div');
+ container.id = 'playwright-highlight-container';
+ container.style.position = 'absolute';
+ container.style.pointerEvents = 'none';
+ container.style.top = '0';
+ container.style.left = '0';
+ container.style.width = '100%';
+ container.style.height = '100%';
+ container.style.zIndex = '2147483647'; // Maximum z-index value
+ document.body.appendChild(container);
+ }
+
+ // Generate a color based on the index
+ const colors = [
+ '#FF0000',
+ '#00FF00',
+ '#0000FF',
+ '#FFA500',
+ '#800080',
+ '#008080',
+ '#FF69B4',
+ '#4B0082',
+ '#FF4500',
+ '#2E8B57',
+ '#DC143C',
+ '#4682B4',
+ ];
+ const colorIndex = index % colors.length;
+ const baseColor = colors[colorIndex];
+ const backgroundColor = `${baseColor}1A`; // 10% opacity version of the color
+
+ // Create highlight overlay
+ const overlay = document.createElement('div');
+ overlay.style.position = 'absolute';
+ overlay.style.border = `2px solid ${baseColor}`;
+ overlay.style.backgroundColor = backgroundColor;
+ overlay.style.pointerEvents = 'none';
+ overlay.style.boxSizing = 'border-box';
+
+ // Position overlay based on element, including scroll position
+ const rect = element.getBoundingClientRect();
+ let top = rect.top + window.scrollY;
+ let left = rect.left + window.scrollX;
+
+ // Adjust position if element is inside an iframe
+ if (parentIframe) {
+ const iframeRect = parentIframe.getBoundingClientRect();
+ top += iframeRect.top;
+ left += iframeRect.left;
+ }
+
+ overlay.style.top = `${top}px`;
+ overlay.style.left = `${left}px`;
+ overlay.style.width = `${rect.width}px`;
+ overlay.style.height = `${rect.height}px`;
+
+ // Create label
+ const label = document.createElement('div');
+ label.className = 'playwright-highlight-label';
+ label.style.position = 'absolute';
+ label.style.background = baseColor;
+ label.style.color = 'white';
+ label.style.padding = '1px 4px';
+ label.style.borderRadius = '4px';
+ label.style.fontSize = `${Math.min(12, Math.max(8, rect.height / 2))}px`; // Responsive font size
+ label.textContent = index;
+
+ // Calculate label position
+ const labelWidth = 20; // Approximate width
+ const labelHeight = 16; // Approximate height
+
+ // Default position (top-right corner inside the box)
+ let labelTop = top + 2;
+ let labelLeft = left + rect.width - labelWidth - 2;
+
+ // Adjust if box is too small
+ if (rect.width < labelWidth + 4 || rect.height < labelHeight + 4) {
+ // Position outside the box if it's too small
+ labelTop = top - labelHeight - 2;
+ labelLeft = left + rect.width - labelWidth;
+ }
+
+ label.style.top = `${labelTop}px`;
+ label.style.left = `${labelLeft}px`;
+
+ // Add to container
+ container.appendChild(overlay);
+ container.appendChild(label);
+
+ // Store reference for cleanup
+ element.setAttribute(
+ 'browser-user-highlight-id',
+ `playwright-highlight-${index}`,
+ );
+
+ return index + 1;
+ }
+
+ // Helper function to generate XPath as a tree
+ function getXPathTree(element, stopAtBoundary = true) {
+ const segments = [];
+ let currentElement = element;
+
+ while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
+ // Stop if we hit a shadow root or iframe
+ if (
+ stopAtBoundary &&
+ (currentElement.parentNode instanceof ShadowRoot ||
+ currentElement.parentNode instanceof HTMLIFrameElement)
+ ) {
+ break;
+ }
+
+ let index = 0;
+ let sibling = currentElement.previousSibling;
+ while (sibling) {
+ if (
+ sibling.nodeType === Node.ELEMENT_NODE &&
+ sibling.nodeName === currentElement.nodeName
+ ) {
+ index++;
+ }
+ sibling = sibling.previousSibling;
+ }
+
+ const tagName = currentElement.nodeName
+ ? currentElement.nodeName.toLowerCase()
+ : null;
+ const xpathIndex = index > 0 ? `[${index + 1}]` : '';
+ segments.unshift(`${tagName}${xpathIndex}`);
+
+ currentElement = currentElement.parentNode;
+ }
+
+ return segments.join('/');
+ }
+
+ function getCssSelector(element) {
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return null;
+
+ // if element has ID, use ID selector (most concise and effective)
+ if (element.id) {
+ return `#${element.id}`;
+ }
+
+ let path = [];
+ while (element && element.nodeType === Node.ELEMENT_NODE) {
+ let selector = element.nodeName ? element.nodeName.toLowerCase() : null;
+
+ // if element has ID, use ID selector
+ if (element.id) {
+ selector = `#${element.id}`;
+ path.unshift(selector);
+ break;
+ }
+ // if element has class name, use class selector (but be careful about multiple class names)
+ else if (element.className && typeof element.className === 'string') {
+ const classes = element.className.trim().split(/\s+/);
+ if (classes.length > 0 && classes[0]) {
+ selector += `.${classes[0]}`;
+ }
+ }
+
+ // add nth-of-type to ensure the selector is unique
+ let sibling = element;
+ let nth = 1;
+ while ((sibling = sibling.previousElementSibling)) {
+ if (
+ sibling.nodeName &&
+ element.nodeName &&
+ sibling.nodeName.toLowerCase() === element.nodeName.toLowerCase()
+ )
+ nth++;
+ }
+ if (nth > 1) selector += `:nth-of-type(${nth})`;
+
+ path.unshift(selector);
+
+ // if we have reached an element with ID or body, stop
+ if (
+ element.parentNode &&
+ (element.parentNode.id ||
+ (element.parentNode.nodeName &&
+ element.parentNode.nodeName.toLowerCase() === 'body'))
+ ) {
+ if (element.parentNode.id) {
+ path.unshift(`#${element.parentNode.id}`);
+ } else {
+ path.unshift('body');
+ }
+ break;
+ }
+
+ element = element.parentNode;
+ }
+
+ return path.join(' > ');
+ }
+ // Helper function to check if element is accepted
+ function isElementAccepted(element) {
+ const leafElementDenyList = new Set([
+ 'svg',
+ 'script',
+ 'style',
+ 'link',
+ 'meta',
+ ]);
+ return !leafElementDenyList.has(
+ element.tagName ? element.tagName.toLowerCase() : null,
+ );
+ }
+
+ // Helper function to check if element is interactive
+ function isInteractiveElement(element) {
+ // Immediately return false for body tag
+ if (element.tagName && element.tagName.toLowerCase() === 'body') {
+ return false;
+ }
+
+ // Base interactive elements and roles
+ const interactiveElements = new Set([
+ 'a',
+ 'button',
+ 'details',
+ 'embed',
+ 'input',
+ 'label',
+ 'menu',
+ 'menuitem',
+ 'object',
+ 'select',
+ 'textarea',
+ 'summary',
+ ]);
+
+ const interactiveRoles = new Set([
+ 'button',
+ 'menu',
+ 'menuitem',
+ 'link',
+ 'checkbox',
+ 'radio',
+ 'slider',
+ 'tab',
+ 'tabpanel',
+ 'textbox',
+ 'combobox',
+ 'grid',
+ 'listbox',
+ 'option',
+ 'progressbar',
+ 'scrollbar',
+ 'searchbox',
+ 'switch',
+ 'tree',
+ 'treeitem',
+ 'spinbutton',
+ 'tooltip',
+ 'a-button-inner',
+ 'a-dropdown-button',
+ 'click',
+ 'menuitemcheckbox',
+ 'menuitemradio',
+ 'a-button-text',
+ 'button-text',
+ 'button-icon',
+ 'button-icon-only',
+ 'button-text-icon-only',
+ 'dropdown',
+ 'combobox',
+ ]);
+
+ const tagName = element.tagName ? element.tagName.toLowerCase() : null;
+ const role = element.getAttribute('role');
+ const ariaRole = element.getAttribute('aria-role');
+ const tabIndex = element.getAttribute('tabindex');
+
+ // Add check for specific class
+ const hasAddressInputClass = element.classList.contains(
+ 'address-input__container__input',
+ );
+
+ // Basic role/attribute checks
+ const hasInteractiveRole =
+ hasAddressInputClass ||
+ interactiveElements.has(tagName) ||
+ interactiveRoles.has(role) ||
+ interactiveRoles.has(ariaRole) ||
+ (tabIndex !== null &&
+ tabIndex !== '-1' &&
+ element.parentElement?.tagName &&
+ element.parentElement?.tagName.toLowerCase() !== 'body') ||
+ element.getAttribute('data-action') === 'a-dropdown-select' ||
+ element.getAttribute('data-action') === 'a-dropdown-button';
+
+ if (hasInteractiveRole) return true;
+
+ // Get computed style
+ const style = window.getComputedStyle(element);
+
+ // Check if element has click-like styling
+ // const hasClickStyling = style.cursor === 'pointer' ||
+ // element.style.cursor === 'pointer' ||
+ // style.pointerEvents !== 'none';
+
+ // Check for event listeners
+ const hasClickHandler =
+ element.onclick !== null ||
+ element.getAttribute('onclick') !== null ||
+ element.hasAttribute('ng-click') ||
+ element.hasAttribute('@click') ||
+ element.hasAttribute('v-on:click');
+
+ // Helper function to safely get event listeners
+ function getEventListeners(el) {
+ try {
+ // Try to get listeners using Chrome DevTools API
+ return window.getEventListeners?.(el) || {};
+ } catch (e) {
+ // Fallback: check for common event properties
+ const listeners = {};
+
+ // List of common event types to check
+ const eventTypes = [
+ 'click',
+ 'mousedown',
+ 'mouseup',
+ 'touchstart',
+ 'touchend',
+ 'keydown',
+ 'keyup',
+ 'focus',
+ 'blur',
+ ];
+
+ for (const type of eventTypes) {
+ const handler = el[`on${type}`];
+ if (handler) {
+ listeners[type] = [
+ {
+ listener: handler,
+ useCapture: false,
+ },
+ ];
+ }
+ }
+
+ return listeners;
+ }
+ }
+
+ // Check for click-related events on the element itself
+ const listeners = getEventListeners(element);
+ const hasClickListeners =
+ listeners &&
+ (listeners.click?.length > 0 ||
+ listeners.mousedown?.length > 0 ||
+ listeners.mouseup?.length > 0 ||
+ listeners.touchstart?.length > 0 ||
+ listeners.touchend?.length > 0);
+
+ // Check for ARIA properties that suggest interactivity
+ const hasAriaProps =
+ element.hasAttribute('aria-expanded') ||
+ element.hasAttribute('aria-pressed') ||
+ element.hasAttribute('aria-selected') ||
+ element.hasAttribute('aria-checked');
+
+ // Check for form-related functionality
+ const isFormRelated =
+ element.form !== undefined ||
+ element.hasAttribute('contenteditable') ||
+ style.userSelect !== 'none';
+
+ // Check if element is draggable
+ const isDraggable =
+ element.draggable || element.getAttribute('draggable') === 'true';
+
+ // Additional check to prevent body from being marked as interactive
+ if (
+ (element.tagName && element.tagName.toLowerCase() === 'body') ||
+ (element.parentElement &&
+ element.parentElement.tagName &&
+ element.parentElement.tagName.toLowerCase() === 'body')
+ ) {
+ return false;
+ }
+
+ return (
+ hasAriaProps ||
+ // hasClickStyling ||
+ hasClickHandler ||
+ hasClickListeners ||
+ // isFormRelated ||
+ isDraggable
+ );
+ }
+
+ // Helper function to check if element is visible
+ function isElementVisible(element) {
+ const style = window.getComputedStyle(element);
+ return (
+ element.offsetWidth > 0 &&
+ element.offsetHeight > 0 &&
+ style.visibility !== 'hidden' &&
+ style.display !== 'none'
+ );
+ }
+
+ // Helper function to check if element is the top element at its position
+ function isTopElement(element) {
+ // Find the correct document context and root element
+ let doc = element.ownerDocument;
+
+ // If we're in an iframe, elements are considered top by default
+ if (doc !== window.document) {
+ return true;
+ }
+
+ // For shadow DOM, we need to check within its own root context
+ const shadowRoot = element.getRootNode();
+ if (shadowRoot instanceof ShadowRoot) {
+ const rect = element.getBoundingClientRect();
+ const point = {
+ x: rect.left + rect.width / 2,
+ y: rect.top + rect.height / 2,
+ };
+
+ try {
+ // Use shadow root's elementFromPoint to check within shadow DOM context
+ const topEl = shadowRoot.elementFromPoint(point.x, point.y);
+ if (!topEl) return false;
+
+ // Check if the element or any of its parents match our target element
+ let current = topEl;
+ while (current && current !== shadowRoot) {
+ if (current === element) return true;
+ current = current.parentElement;
+ }
+ return false;
+ } catch (e) {
+ return true; // If we can't determine, consider it visible
+ }
+ }
+
+ // Regular DOM elements
+ const rect = element.getBoundingClientRect();
+
+ // If viewportExpansion is -1, check if element is the top one at its position
+ if (viewportExpansion === -1) {
+ return true; // Consider all elements as top elements when expansion is -1
+ }
+
+ // Calculate expanded viewport boundaries including scroll position
+ const scrollX = window.scrollX;
+ const scrollY = window.scrollY;
+ const viewportTop = -viewportExpansion + scrollY;
+ const viewportLeft = -viewportExpansion + scrollX;
+ const viewportBottom = window.innerHeight + viewportExpansion + scrollY;
+ const viewportRight = window.innerWidth + viewportExpansion + scrollX;
+
+ // Get absolute element position
+ const absTop = rect.top + scrollY;
+ const absLeft = rect.left + scrollX;
+ const absBottom = rect.bottom + scrollY;
+ const absRight = rect.right + scrollX;
+
+ // Skip if element is completely outside expanded viewport
+ if (
+ absBottom < viewportTop ||
+ absTop > viewportBottom ||
+ absRight < viewportLeft ||
+ absLeft > viewportRight
+ ) {
+ return false;
+ }
+
+ // For elements within expanded viewport, check if they're the top element
+ try {
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+
+ // Only clamp the point if it's outside the actual document
+ const point = {
+ x: centerX,
+ y: centerY,
+ };
+
+ if (
+ point.x < 0 ||
+ point.x >= window.innerWidth ||
+ point.y < 0 ||
+ point.y >= window.innerHeight
+ ) {
+ return true; // Consider elements with center outside viewport as visible
+ }
+
+ const topEl = document.elementFromPoint(point.x, point.y);
+ if (!topEl) return false;
+
+ let current = topEl;
+ while (current && current !== document.documentElement) {
+ if (current === element) return true;
+ current = current.parentElement;
+ }
+ return false;
+ } catch (e) {
+ return true;
+ }
+ }
+
+ // Helper function to check if text node is visible
+ function isTextNodeVisible(textNode) {
+ const range = document.createRange();
+ range.selectNodeContents(textNode);
+ const rect = range.getBoundingClientRect();
+
+ return (
+ rect.width !== 0 &&
+ rect.height !== 0 &&
+ rect.top >= 0 &&
+ rect.top <= window.innerHeight &&
+ textNode.parentElement &&
+ textNode.parentElement.checkVisibility({
+ checkOpacity: true,
+ checkVisibilityCSS: true,
+ })
+ );
+ }
+
+ // Function to traverse the DOM and create nested JSON
+ function buildDomTree(node, parentIframe = null) {
+ if (!node) return null;
+
+ // Special case for text nodes
+ if (node.nodeType === Node.TEXT_NODE) {
+ const textContent = node.textContent.trim();
+ if (textContent && isTextNodeVisible(node)) {
+ return {
+ type: 'TEXT_NODE',
+ text: textContent,
+ isVisible: true,
+ };
+ }
+ return null;
+ }
+
+ // Check if element is accepted
+ if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
+ return null;
+ }
+
+ const nodeData = {
+ tagName: node.tagName ? node.tagName.toLowerCase() : null,
+ attributes: {},
+ xpath:
+ node.nodeType === Node.ELEMENT_NODE ? getXPathTree(node, true) : null,
+ cssSelector:
+ node.nodeType === Node.ELEMENT_NODE ? getCssSelector(node) : null,
+ children: [],
+ };
+
+ // Add coordinates for element nodes
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const rect = node.getBoundingClientRect();
+ const scrollX = window.scrollX;
+ const scrollY = window.scrollY;
+
+ // Viewport-relative coordinates (can be negative when scrolled)
+ nodeData.viewportCoordinates = {
+ topLeft: {
+ x: Math.round(rect.left),
+ y: Math.round(rect.top),
+ },
+ topRight: {
+ x: Math.round(rect.right),
+ y: Math.round(rect.top),
+ },
+ bottomLeft: {
+ x: Math.round(rect.left),
+ y: Math.round(rect.bottom),
+ },
+ bottomRight: {
+ x: Math.round(rect.right),
+ y: Math.round(rect.bottom),
+ },
+ center: {
+ x: Math.round(rect.left + rect.width / 2),
+ y: Math.round(rect.top + rect.height / 2),
+ },
+ width: Math.round(rect.width),
+ height: Math.round(rect.height),
+ };
+
+ // Page-relative coordinates (always positive, relative to page origin)
+ nodeData.pageCoordinates = {
+ topLeft: {
+ x: Math.round(rect.left + scrollX),
+ y: Math.round(rect.top + scrollY),
+ },
+ topRight: {
+ x: Math.round(rect.right + scrollX),
+ y: Math.round(rect.top + scrollY),
+ },
+ bottomLeft: {
+ x: Math.round(rect.left + scrollX),
+ y: Math.round(rect.bottom + scrollY),
+ },
+ bottomRight: {
+ x: Math.round(rect.right + scrollX),
+ y: Math.round(rect.bottom + scrollY),
+ },
+ center: {
+ x: Math.round(rect.left + rect.width / 2 + scrollX),
+ y: Math.round(rect.top + rect.height / 2 + scrollY),
+ },
+ width: Math.round(rect.width),
+ height: Math.round(rect.height),
+ };
+
+ // Add viewport and scroll information
+ nodeData.viewport = {
+ scrollX: Math.round(scrollX),
+ scrollY: Math.round(scrollY),
+ width: window.innerWidth,
+ height: window.innerHeight,
+ };
+ }
+
+ // Copy all attributes if the node is an element
+ if (node.nodeType === Node.ELEMENT_NODE && node.attributes) {
+ // Use getAttributeNames() instead of directly iterating attributes
+ const attributeNames = node.getAttributeNames?.() || [];
+ for (const name of attributeNames) {
+ nodeData.attributes[name] = node.getAttribute(name);
+ }
+ }
+
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const isInteractive = isInteractiveElement(node);
+ const isVisible = isElementVisible(node);
+ const isTop = isTopElement(node);
+
+ nodeData.isInteractive = isInteractive;
+ nodeData.isVisible = isVisible;
+ nodeData.isTopElement = isTop;
+
+ // Highlight if element meets all criteria and highlighting is enabled
+ if (isInteractive && isVisible && isTop) {
+ nodeData.highlightIndex = highlightIndex++;
+ if (doHighlightElements) {
+ if (focusHighlightIndex >= 0) {
+ if (focusHighlightIndex === nodeData.highlightIndex) {
+ highlightElement(node, nodeData.highlightIndex, parentIframe);
+ }
+ } else {
+ highlightElement(node, nodeData.highlightIndex, parentIframe);
+ }
+ }
+ }
+ }
+
+ // Only add iframeContext if we're inside an iframe
+ // if (parentIframe) {
+ // nodeData.iframeContext = `iframe[src="${parentIframe.src || ''}"]`;
+ // }
+
+ // Only add shadowRoot field if it exists
+ if (node.shadowRoot) {
+ nodeData.shadowRoot = true;
+ }
+
+ // Handle shadow DOM
+ if (node.shadowRoot) {
+ const shadowChildren = Array.from(node.shadowRoot.childNodes).map(
+ (child) => buildDomTree(child, parentIframe),
+ );
+ nodeData.children.push(...shadowChildren);
+ }
+
+ // Handle iframes
+ if (node.tagName === 'IFRAME') {
+ try {
+ const iframeDoc = node.contentDocument || node.contentWindow.document;
+ if (iframeDoc) {
+ const iframeChildren = Array.from(iframeDoc.body.childNodes).map(
+ (child) => buildDomTree(child, node),
+ );
+ nodeData.children.push(...iframeChildren);
+ }
+ } catch (e) {
+ console.warn('Unable to access iframe:', node);
+ }
+ } else {
+ const children = Array.from(node.childNodes).map((child) =>
+ buildDomTree(child, parentIframe),
+ );
+ nodeData.children.push(...children);
+ }
+
+ return nodeData;
+ }
+
+ return buildDomTree(document.body);
+};
diff --git a/packages/agent-infra/browser-use/package.json b/packages/agent-infra/browser-use/package.json
new file mode 100644
index 000000000..0091b6d99
--- /dev/null
+++ b/packages/agent-infra/browser-use/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "@agent-infra/browser-use",
+ "version": "0.0.1",
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "files": [
+ "dist",
+ "assets"
+ ],
+ "scripts": {
+ "dev": "rslib build --watch",
+ "build": "rslib build",
+ "prepare": "npm run build",
+ "prepublishOnly": "pnpm run build",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:e2e": "vitest --config vitest.e2e.config.ts",
+ "coverage": "vitest run --coverage",
+ "test:e2e:local": "vitest --config vitest.e2e.config.ts local-browser.e2e.test.ts"
+ },
+ "dependencies": {
+ "zod": "^3.23.8",
+ "openai": "^4.87.3",
+ "jsonrepair": "3.12.0",
+ "@langchain/core": "0.3.42",
+ "puppeteer-core": "24.1.1",
+ "@agent-infra/browser": "workspace:*",
+ "@agent-infra/logger": "workspace:*",
+ "@agent-infra/shared": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/node": "20.14.8",
+ "@langchain/aws": "0.1.6",
+ "@langchain/openai": "0.4.4",
+ "typescript": "^5.7.3",
+ "tsx": "^4.19.3",
+ "vitest": "3.0.7",
+ "@vitest/coverage-v8": "3.0.7",
+ "@rslib/core": "0.5.3"
+ }
+}
diff --git a/packages/agent-infra/browser-use/rslib.config.ts b/packages/agent-infra/browser-use/rslib.config.ts
new file mode 100644
index 000000000..5bf5cfd7e
--- /dev/null
+++ b/packages/agent-infra/browser-use/rslib.config.ts
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2025 Bytedance, Inc. and its affiliates.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { defineConfig } from '@rslib/core';
+import path from 'path';
+import fs from 'fs';
+
+const BANNER = `/**
+* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
+* SPDX-License-Identifier: Apache-2.0
+*/`;
+
+export default defineConfig({
+ source: {
+ entry: {
+ index: ['src/**/*.ts', '!src/**/*.{test,bench}.ts'],
+ },
+ define: {
+ BUILD_DOM_TREE_SCRIPT: JSON.stringify(
+ fs.readFileSync(
+ path.join(__dirname, './assets/buildDomTree.js'),
+ 'utf8',
+ ),
+ ),
+ },
+ },
+ lib: [
+ {
+ format: 'esm',
+ syntax: 'es2021',
+ bundle: false,
+ dts: true,
+ banner: { js: BANNER },
+ },
+ {
+ format: 'cjs',
+ syntax: 'es2021',
+ bundle: false,
+ dts: true,
+ banner: { js: BANNER },
+ },
+ ],
+ output: {
+ target: 'web',
+ cleanDistPath: true,
+ sourceMap: true,
+ },
+});
diff --git a/packages/agent-infra/browser-use/src/agent/actions/builder.ts b/packages/agent-infra/browser-use/src/agent/actions/builder.ts
new file mode 100644
index 000000000..adc9374ba
--- /dev/null
+++ b/packages/agent-infra/browser-use/src/agent/actions/builder.ts
@@ -0,0 +1,509 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/src/background/agent/actions/builder.ts
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ */
+import { ActionResult, type AgentContext } from '../types';
+import {
+ clickElementActionSchema,
+ doneActionSchema,
+ extractContentActionSchema,
+ goBackActionSchema,
+ goToUrlActionSchema,
+ inputTextActionSchema,
+ openTabActionSchema,
+ searchGoogleActionSchema,
+ switchTabActionSchema,
+ type ActionSchema,
+ scrollDownActionSchema,
+ scrollUpActionSchema,
+ sendKeysActionSchema,
+ scrollToTextActionSchema,
+ cacheContentActionSchema,
+} from './schemas';
+import { z } from 'zod';
+import { createLogger } from '../../utils';
+import { PromptTemplate } from '@langchain/core/prompts';
+import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
+import { ExecutionState, Actors } from '../event/types';
+
+const logger = createLogger('Action');
+
+export class InvalidInputError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'InvalidInputError';
+ }
+}
+
+/**
+ * An action is a function that takes an input and returns an ActionResult
+ */
+export class Action {
+ constructor(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private readonly handler: (input: any) => Promise,
+ public readonly schema: ActionSchema,
+ ) {}
+
+ async call(input: unknown): Promise {
+ // Validate input before calling the handler
+ const schema = this.schema.schema;
+
+ // check if the schema is schema: z.object({}), if so, ignore the input
+ const isEmptySchema =
+ schema instanceof z.ZodObject &&
+ Object.keys(
+ (schema as z.ZodObject>).shape || {},
+ ).length === 0;
+
+ if (isEmptySchema) {
+ return await this.handler({});
+ }
+
+ const parsedArgs = this.schema.schema.safeParse(input);
+ if (!parsedArgs.success) {
+ const errorMessage = parsedArgs.error.message;
+ throw new InvalidInputError(errorMessage);
+ }
+ return await this.handler(parsedArgs.data);
+ }
+
+ name() {
+ return this.schema.name;
+ }
+
+ /**
+ * Returns the prompt for the action
+ * @returns {string} The prompt for the action
+ */
+ prompt() {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const schemaShape = (this.schema.schema as z.ZodObject).shape || {};
+ const schemaProperties = Object.entries(schemaShape).map(([key, value]) => {
+ const zodValue = value as z.ZodTypeAny;
+ return `'${key}': {'type': '${zodValue.description}', ${zodValue.isOptional() ? "'optional': true" : "'required': true"}}`;
+ });
+
+ const schemaStr =
+ schemaProperties.length > 0
+ ? `{${this.name()}: {${schemaProperties.join(', ')}}}`
+ : `{${this.name()}: {}}`;
+
+ return `${this.schema.description}:\n${schemaStr}`;
+ }
+}
+
+// TODO: can not make every action optional, don't know why
+export function buildDynamicActionSchema(actions: Action[]): z.ZodType {
+ let schema = z.object({});
+ for (const action of actions) {
+ // create a schema for the action, it could be action.schema.schema or null
+ // but don't use default: null as it causes issues with Google Generative AI
+ const actionSchema = action.schema.schema.nullable();
+ schema = schema.extend({
+ [action.name()]: actionSchema,
+ });
+ }
+ return schema.partial().nullable();
+}
+
+export class ActionBuilder {
+ private readonly context: AgentContext;
+ private readonly extractorLLM: BaseChatModel;
+
+ constructor(context: AgentContext, extractorLLM: BaseChatModel) {
+ this.context = context;
+ this.extractorLLM = extractorLLM;
+ }
+
+ buildDefaultActions() {
+ const actions = [];
+
+ const done = new Action(
+ async (input: z.infer) => {
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ doneActionSchema.name,
+ );
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_OK,
+ input.text,
+ );
+ return new ActionResult({
+ isDone: true,
+ extractedContent: input.text,
+ });
+ },
+ doneActionSchema,
+ );
+ actions.push(done);
+
+ const searchGoogle = new Action(async (input: { query: string }) => {
+ const msg = `Searching for "${input.query}" in Google`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_START, msg);
+
+ const page = await this.context.browserContext.getCurrentPage();
+ await page.navigateTo(`https://www.google.com/search?q=${input.query}`);
+
+ const msg2 = `Searched for "${input.query}" in Google`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg2);
+ return new ActionResult({
+ extractedContent: msg2,
+ includeInMemory: true,
+ });
+ }, searchGoogleActionSchema);
+ actions.push(searchGoogle);
+
+ const goToUrl = new Action(async (input: { url: string }) => {
+ const msg = `Navigating to ${input.url}`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_START, msg);
+
+ await this.context.browserContext.navigateTo(input.url);
+ const msg2 = `Navigated to ${input.url}`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg2);
+ return new ActionResult({
+ extractedContent: msg2,
+ includeInMemory: true,
+ });
+ }, goToUrlActionSchema);
+ actions.push(goToUrl);
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const goBack = new Action(async (_input = {}) => {
+ const msg = 'Navigating back';
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_START, msg);
+
+ const page = await this.context.browserContext.getCurrentPage();
+ await page.goBack();
+ const msg2 = 'Navigated back';
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg2);
+ return new ActionResult({
+ extractedContent: msg2,
+ includeInMemory: true,
+ });
+ }, goBackActionSchema);
+ actions.push(goBack);
+
+ // Element Interaction Actions
+ const clickElement = new Action(
+ async (input: z.infer) => {
+ const todo = input.desc || `Click element with index ${input.index}`;
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ todo,
+ );
+
+ const page = await this.context.browserContext.getCurrentPage();
+ const state = await page.getState();
+
+ const elementNode = state?.selectorMap.get(input.index);
+ if (!elementNode) {
+ throw new Error(
+ `Element with index ${input.index} does not exist - retry or use alternative actions`,
+ );
+ }
+
+ // Check if element is a file uploader
+ if (await page.isFileUploader(elementNode)) {
+ const msg = `Index ${input.index} - has an element which opens file upload dialog. To upload files please use a specific function to upload files`;
+ logger.info(msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ }
+
+ try {
+ // const initialTabIds =
+ // await this.context.browserContext.getAllTabIds();
+ console.log('elementNode', elementNode);
+ await page.clickElementNode(
+ this.context.options.useVision,
+ elementNode,
+ );
+ const msg = `Clicked button with index ${input.index}: ${elementNode.getAllTextTillNextClickableElement(2)}`;
+ logger.info(msg);
+
+ // TODO: could be optimized by chrome extension tab api
+ // const currentTabIds =
+ // await this.context.browserContext.getAllTabIds();
+ // if (currentTabIds.size > initialTabIds.size) {
+ // const newTabMsg = 'New tab opened - switching to it';
+ // msg += ` - ${newTabMsg}`;
+ // logger.info(newTabMsg);
+ // // find the tab id that is not in the initial tab ids
+ // const newTabId = Array.from(currentTabIds).find(
+ // (id) => !initialTabIds.has(id),
+ // );
+ // if (newTabId) {
+ // await this.context.browserContext.switchTab(newTabId);
+ // }
+ // }
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ } catch (error) {
+ const msg = `Element no longer available with index ${input.index} - most likely the page changed`;
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_FAIL,
+ msg,
+ );
+ return new ActionResult({
+ error: error instanceof Error ? error.message : String(error),
+ });
+ }
+ },
+ clickElementActionSchema,
+ );
+ actions.push(clickElement);
+
+ const inputText = new Action(
+ async (input: z.infer) => {
+ const todo = input.desc || `Input text into index ${input.index}`;
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ todo,
+ );
+
+ const page = await this.context.browserContext.getCurrentPage();
+ const state = await page.getState();
+
+ const elementNode = state?.selectorMap.get(input.index);
+ if (!elementNode) {
+ throw new Error(
+ `Element with index ${input.index} does not exist - retry or use alternative actions`,
+ );
+ }
+
+ await page.inputTextElementNode(
+ this.context.options.useVision,
+ elementNode,
+ input.text,
+ );
+ const msg = `Input ${input.text} into index ${input.index}`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ },
+ inputTextActionSchema,
+ );
+ actions.push(inputText);
+
+ // Tab Management Actions
+ const switchTab = new Action(
+ async (input: z.infer) => {
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ `Switching to tab ${input.tab_id}`,
+ );
+ await this.context.browserContext.switchTab(input.tab_id);
+ const msg = `Switched to tab ${input.tab_id}`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ },
+ switchTabActionSchema,
+ );
+ actions.push(switchTab);
+
+ const openTab = new Action(
+ async (input: z.infer) => {
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ `Opening ${input.url} in new tab`,
+ );
+ await this.context.browserContext.openTab(input.url);
+ const msg = `Opened ${input.url} in new tab`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ },
+ openTabActionSchema,
+ );
+ actions.push(openTab);
+
+ // Content Actions
+ // TODO: this is not used currently, need to improve on input size
+ const extractContent = new Action(
+ async (input: z.infer) => {
+ const goal = input.goal;
+ const page = await this.context.browserContext.getCurrentPage();
+ const content = await page.getReadabilityContent();
+ const promptTemplate = PromptTemplate.fromTemplate(
+ 'Your task is to extract the content of the page. You will be given a page and a goal and you should extract all relevant information around this goal from the page. If the goal is vague, summarize the page. Respond in json format. Extraction goal: {goal}, Page: {page}',
+ );
+ const prompt = await promptTemplate.invoke({
+ goal,
+ page: content?.content,
+ });
+
+ try {
+ const output = await this.extractorLLM.invoke(prompt);
+ const msg = `📄 Extracted from page\n: ${output.content}\n`;
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ } catch (error) {
+ logger.error(
+ `Error extracting content: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ const msg =
+ 'Failed to extract content from page, you need to extract content from the current state of the page and store it in the memory. Then scroll down if you still need more information.';
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ }
+ },
+ extractContentActionSchema,
+ );
+ actions.push(extractContent);
+
+ // cache content for future use
+ const cacheContent = new Action(
+ async (input: z.infer) => {
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ cacheContentActionSchema.name,
+ );
+
+ const msg = `Cached findings: ${input.content}`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ },
+ cacheContentActionSchema,
+ );
+ actions.push(cacheContent);
+
+ const scrollDown = new Action(
+ async (input: z.infer) => {
+ const todo = input.desc || 'Scroll down the page';
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ todo,
+ );
+
+ const page = await this.context.browserContext.getCurrentPage();
+ await page.scrollDown(input.amount);
+ const amount =
+ input.amount !== undefined ? `${input.amount} pixels` : 'one page';
+ const msg = `Scrolled down the page by ${amount}`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ },
+ scrollDownActionSchema,
+ );
+ actions.push(scrollDown);
+
+ const scrollUp = new Action(
+ async (input: z.infer) => {
+ const todo = input.desc || 'Scroll up the page';
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ todo,
+ );
+
+ const page = await this.context.browserContext.getCurrentPage();
+ await page.scrollUp(input.amount);
+ const amount =
+ input.amount !== undefined ? `${input.amount} pixels` : 'one page';
+ const msg = `Scrolled up the page by ${amount}`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ },
+ scrollUpActionSchema,
+ );
+ actions.push(scrollUp);
+
+ // Keyboard Actions
+ const sendKeys = new Action(
+ async (input: z.infer) => {
+ const todo = input.desc || `Send keys: ${input.keys}`;
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ todo,
+ );
+
+ const page = await this.context.browserContext.getCurrentPage();
+ await page.sendKeys(input.keys);
+ const msg = `Sent keys: ${input.keys}`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ },
+ sendKeysActionSchema,
+ );
+ actions.push(sendKeys);
+
+ const scrollToText = new Action(
+ async (input: z.infer) => {
+ const todo = input.desc || `Scroll to text: ${input.text}`;
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_START,
+ todo,
+ );
+
+ const page = await this.context.browserContext.getCurrentPage();
+ try {
+ const scrolled = await page.scrollToText(input.text);
+ const msg = scrolled
+ ? `Scrolled to text: ${input.text}`
+ : `Text '${input.text}' not found or not visible on page`;
+ this.context.emitEvent(Actors.NAVIGATOR, ExecutionState.ACT_OK, msg);
+ return new ActionResult({
+ extractedContent: msg,
+ includeInMemory: true,
+ });
+ } catch (error) {
+ const msg = `Failed to scroll to text: ${error instanceof Error ? error.message : String(error)}`;
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_FAIL,
+ msg,
+ );
+ return new ActionResult({ error: msg, includeInMemory: true });
+ }
+ },
+ scrollToTextActionSchema,
+ );
+ actions.push(scrollToText);
+
+ return actions;
+ }
+}
diff --git a/packages/agent-infra/browser-use/src/agent/actions/json_gemini.ts b/packages/agent-infra/browser-use/src/agent/actions/json_gemini.ts
new file mode 100644
index 000000000..0b1727c69
--- /dev/null
+++ b/packages/agent-infra/browser-use/src/agent/actions/json_gemini.ts
@@ -0,0 +1,240 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/src/background/agent/actions/json_gemini.ts
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ */
+// TODO: don't know why zod can not generate the same schema, need to fix it
+export const geminiNavigatorOutputSchema = {
+ type: 'object',
+ properties: {
+ current_state: {
+ type: 'object',
+ description: 'Current state of the agent',
+ properties: {
+ page_summary: {
+ type: 'string',
+ },
+ evaluation_previous_goal: {
+ type: 'string',
+ },
+ memory: {
+ type: 'string',
+ },
+ next_goal: {
+ type: 'string',
+ },
+ },
+ required: [
+ 'page_summary',
+ 'evaluation_previous_goal',
+ 'memory',
+ 'next_goal',
+ ],
+ },
+ action: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ done: {
+ type: 'object',
+ properties: {
+ text: {
+ type: 'string',
+ },
+ },
+ required: ['text'],
+ nullable: true,
+ },
+ search_google: {
+ type: 'object',
+ properties: {
+ query: {
+ type: 'string',
+ },
+ },
+ required: ['query'],
+ nullable: true,
+ },
+ go_to_url: {
+ type: 'object',
+ properties: {
+ url: {
+ type: 'string',
+ },
+ },
+ required: ['url'],
+ nullable: true,
+ },
+ go_back: {
+ type: 'string',
+ nullable: true,
+ description:
+ 'Accepts absolutely anything in the incoming data\nand discards it, so the final parsed model is empty.',
+ },
+ click_element: {
+ type: 'object',
+ properties: {
+ desc: {
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ index: {
+ type: 'integer',
+ },
+ xpath: {
+ type: 'string',
+ nullable: true,
+ },
+ },
+ required: ['desc', 'index'],
+ nullable: true,
+ },
+ input_text: {
+ type: 'object',
+ properties: {
+ desc: {
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ index: {
+ type: 'integer',
+ },
+ text: {
+ type: 'string',
+ },
+ xpath: {
+ type: 'string',
+ nullable: true,
+ },
+ },
+ required: ['desc', 'index', 'text'],
+ nullable: true,
+ },
+ switch_tab: {
+ type: 'object',
+ properties: {
+ tab_id: {
+ type: 'integer',
+ },
+ },
+ required: ['tab_id'],
+ nullable: true,
+ },
+ open_tab: {
+ type: 'object',
+ properties: {
+ url: {
+ type: 'string',
+ },
+ },
+ required: ['url'],
+ nullable: true,
+ },
+ cache_content: {
+ type: 'object',
+ properties: {
+ content: {
+ type: 'string',
+ },
+ },
+ required: ['content'],
+ nullable: true,
+ },
+ scroll_down: {
+ type: 'object',
+ properties: {
+ desc: {
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ amount: {
+ type: 'integer',
+ nullable: true,
+ },
+ },
+ required: ['desc'],
+ nullable: true,
+ },
+ scroll_up: {
+ type: 'object',
+ properties: {
+ desc: {
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ amount: {
+ type: 'integer',
+ nullable: true,
+ },
+ },
+ required: ['desc'],
+ nullable: true,
+ },
+ send_keys: {
+ type: 'object',
+ properties: {
+ desc: {
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ keys: {
+ type: 'string',
+ },
+ },
+ required: ['desc', 'keys'],
+ nullable: true,
+ },
+ scroll_to_text: {
+ type: 'object',
+ properties: {
+ desc: {
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ text: {
+ type: 'string',
+ },
+ },
+ required: ['desc', 'text'],
+ nullable: true,
+ },
+ get_dropdown_options: {
+ type: 'object',
+ properties: {
+ index: {
+ type: 'integer',
+ },
+ },
+ required: ['index'],
+ nullable: true,
+ },
+ select_dropdown_option: {
+ type: 'object',
+ properties: {
+ index: {
+ type: 'integer',
+ },
+ text: {
+ type: 'string',
+ },
+ },
+ required: ['index', 'text'],
+ nullable: true,
+ },
+ },
+ required: [],
+ },
+ },
+ },
+ required: ['current_state', 'action'],
+};
diff --git a/packages/agent-infra/browser-use/src/agent/actions/json_schema.ts b/packages/agent-infra/browser-use/src/agent/actions/json_schema.ts
new file mode 100644
index 000000000..59a00c067
--- /dev/null
+++ b/packages/agent-infra/browser-use/src/agent/actions/json_schema.ts
@@ -0,0 +1,289 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/src/background/agent/actions/json_schema.ts
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ */
+// This is the json schema exported from browser-use, change page_id to tab_id
+// TODO: don't know why zod can not generate the same schema, need to fix it
+export const jsonNavigatorOutputSchema = {
+ properties: {
+ current_state: {
+ description: 'Current state of the agent',
+ properties: {
+ page_summary: {
+ title: 'Page Summary',
+ type: 'string',
+ },
+ evaluation_previous_goal: {
+ title: 'Evaluation Previous Goal',
+ type: 'string',
+ },
+ memory: {
+ title: 'Memory',
+ type: 'string',
+ },
+ next_goal: {
+ title: 'Next Goal',
+ type: 'string',
+ },
+ },
+ required: [
+ 'page_summary',
+ 'evaluation_previous_goal',
+ 'memory',
+ 'next_goal',
+ ],
+ title: 'AgentBrain',
+ type: 'object',
+ },
+ action: {
+ items: {
+ properties: {
+ done: {
+ properties: {
+ text: {
+ title: 'Text',
+ type: 'string',
+ },
+ },
+ required: ['text'],
+ title: 'DoneAction',
+ type: 'object',
+ nullable: true,
+ },
+ search_google: {
+ properties: {
+ query: {
+ title: 'Query',
+ type: 'string',
+ },
+ },
+ required: ['query'],
+ title: 'SearchGoogleAction',
+ type: 'object',
+ nullable: true,
+ },
+ go_to_url: {
+ properties: {
+ url: {
+ title: 'Url',
+ type: 'string',
+ },
+ },
+ required: ['url'],
+ title: 'GoToUrlAction',
+ type: 'object',
+ nullable: true,
+ },
+ go_back: {
+ additionalProperties: true,
+ description:
+ 'Accepts absolutely anything in the incoming data\nand discards it, so the final parsed model is empty.',
+ properties: {},
+ title: 'NoParamsAction',
+ type: 'object',
+ nullable: true,
+ },
+ click_element: {
+ properties: {
+ desc: {
+ title: 'Intent',
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ index: {
+ title: 'Index',
+ type: 'integer',
+ },
+ xpath: {
+ title: 'XPath',
+ type: 'string',
+ nullable: true,
+ },
+ },
+ required: ['desc', 'index'],
+ title: 'ClickElementAction',
+ type: 'object',
+ nullable: true,
+ },
+ input_text: {
+ properties: {
+ desc: {
+ title: 'Intent',
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ index: {
+ title: 'Index',
+ type: 'integer',
+ },
+ text: {
+ title: 'Text',
+ type: 'string',
+ },
+ xpath: {
+ title: 'XPath',
+ type: 'string',
+ nullable: true,
+ },
+ },
+ required: ['desc', 'index', 'text'],
+ title: 'InputTextAction',
+ type: 'object',
+ nullable: true,
+ },
+ switch_tab: {
+ properties: {
+ tab_id: {
+ title: 'Page Id',
+ type: 'integer',
+ },
+ },
+ required: ['tab_id'],
+ title: 'SwitchTabAction',
+ type: 'object',
+ nullable: true,
+ },
+ open_tab: {
+ properties: {
+ url: {
+ title: 'Url',
+ type: 'string',
+ },
+ },
+ required: ['url'],
+ title: 'OpenTabAction',
+ type: 'object',
+ nullable: true,
+ },
+ cache_content: {
+ properties: {
+ content: {
+ title: 'Content',
+ type: 'string',
+ },
+ },
+ required: ['content'],
+ title: 'cache_content_parameters',
+ type: 'object',
+ nullable: true,
+ },
+ scroll_down: {
+ properties: {
+ desc: {
+ title: 'Intent',
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ amount: {
+ title: 'Amount',
+ type: 'integer',
+ nullable: true,
+ },
+ },
+ required: ['desc'],
+ title: 'ScrollAction',
+ type: 'object',
+ nullable: true,
+ },
+ scroll_up: {
+ properties: {
+ desc: {
+ title: 'Intent',
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ amount: {
+ title: 'Amount',
+ type: 'integer',
+ nullable: true,
+ },
+ },
+ required: ['desc'],
+ title: 'ScrollAction',
+ type: 'object',
+ nullable: true,
+ },
+ send_keys: {
+ properties: {
+ desc: {
+ title: 'Intent',
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ keys: {
+ title: 'Keys',
+ type: 'string',
+ },
+ },
+ required: ['desc', 'keys'],
+ title: 'SendKeysAction',
+ type: 'object',
+ nullable: true,
+ },
+ scroll_to_text: {
+ properties: {
+ desc: {
+ title: 'Intent',
+ type: 'string',
+ description:
+ 'Very short explanation of the intent or purpose for calling this action',
+ },
+ text: {
+ title: 'Text',
+ type: 'string',
+ },
+ },
+ required: ['desc', 'text'],
+ title: 'scroll_to_text_parameters',
+ type: 'object',
+ nullable: true,
+ },
+ get_dropdown_options: {
+ properties: {
+ index: {
+ title: 'Index',
+ type: 'integer',
+ },
+ },
+ required: ['index'],
+ title: 'get_dropdown_options_parameters',
+ type: 'object',
+ nullable: true,
+ },
+ select_dropdown_option: {
+ properties: {
+ index: {
+ title: 'Index',
+ type: 'integer',
+ },
+ text: {
+ title: 'Text',
+ type: 'string',
+ },
+ },
+ required: ['index', 'text'],
+ title: 'select_dropdown_option_parameters',
+ type: 'object',
+ nullable: true,
+ },
+ },
+ title: 'ActionModel',
+ type: 'object',
+ },
+ title: 'Action',
+ type: 'array',
+ },
+ },
+ required: ['current_state', 'action'],
+ title: 'AgentOutput',
+ type: 'object',
+};
diff --git a/packages/agent-infra/browser-use/src/agent/actions/schemas.ts b/packages/agent-infra/browser-use/src/agent/actions/schemas.ts
new file mode 100644
index 000000000..226683080
--- /dev/null
+++ b/packages/agent-infra/browser-use/src/agent/actions/schemas.ts
@@ -0,0 +1,161 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/src/background/agent/actions/schemas.ts
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ */
+import { z } from 'zod';
+
+export interface ActionSchema {
+ name: string;
+ description: string;
+ schema: z.ZodType;
+}
+
+export const doneActionSchema: ActionSchema = {
+ name: 'done',
+ description: 'Complete task',
+ schema: z.object({
+ text: z.string(),
+ }),
+};
+
+// Basic Navigation Actions
+export const searchGoogleActionSchema: ActionSchema = {
+ name: 'search_google',
+ description: 'Search Google in the current tab',
+ schema: z.object({
+ query: z.string(),
+ }),
+};
+
+export const goToUrlActionSchema: ActionSchema = {
+ name: 'go_to_url',
+ description: 'Navigate to URL in the current tab',
+ schema: z.object({
+ url: z.string(),
+ }),
+};
+
+export const goBackActionSchema: ActionSchema = {
+ name: 'go_back',
+ description: 'Go back to the previous page',
+ schema: z.object({}),
+};
+
+export const clickElementActionSchema: ActionSchema = {
+ name: 'click_element',
+ description: 'Click element',
+ schema: z.object({
+ desc: z.string().optional(), // some small LLM can not generate a description, so let it be optional (but it's still makred as required in json schema)
+ index: z.number(),
+ xpath: z.string().optional(),
+ }),
+};
+
+export const inputTextActionSchema: ActionSchema = {
+ name: 'input_text',
+ description: 'Input text into an interactive input element',
+ schema: z.object({
+ desc: z.string().optional(),
+ index: z.number(),
+ text: z.string(),
+ xpath: z.string().optional(),
+ }),
+};
+
+// Tab Management Actions
+export const switchTabActionSchema: ActionSchema = {
+ name: 'switch_tab',
+ description: 'Switch to tab by id',
+ schema: z.object({
+ tab_id: z.number(),
+ }),
+};
+
+export const openTabActionSchema: ActionSchema = {
+ name: 'open_tab',
+ description: 'Open URL in new tab',
+ schema: z.object({
+ url: z.string(),
+ }),
+};
+
+// Content Actions
+export const extractContentActionSchema: ActionSchema = {
+ name: 'extract_content',
+ description:
+ 'Extract page content to retrieve specific information from the page, e.g. all company names, a specifc description, all information about, links with companies in structured format or simply links',
+ schema: z.object({
+ goal: z.string(),
+ }),
+};
+
+// Cache Actions
+export const cacheContentActionSchema: ActionSchema = {
+ name: 'cache_content',
+ description: 'Cache the extracted content of the page',
+ schema: z.object({
+ content: z.string(),
+ }),
+};
+
+export const scrollDownActionSchema: ActionSchema = {
+ name: 'scroll_down',
+ description:
+ 'Scroll down the page by pixel amount - if no amount is specified, scroll down one page',
+ schema: z.object({
+ desc: z.string().optional(),
+ amount: z.number().optional(),
+ }),
+};
+
+export const scrollUpActionSchema: ActionSchema = {
+ name: 'scroll_up',
+ description:
+ 'Scroll up the page by pixel amount - if no amount is specified, scroll up one page',
+ schema: z.object({
+ desc: z.string().optional(),
+ amount: z.number().optional(),
+ }),
+};
+
+export const sendKeysActionSchema: ActionSchema = {
+ name: 'send_keys',
+ description:
+ 'Send strings of special keys like Backspace, Insert, PageDown, Delete, Enter. Shortcuts such as `Control+o`, `Control+Shift+T` are supported as well. This gets used in keyboard press. Be aware of different operating systems and their shortcuts',
+ schema: z.object({
+ desc: z.string().optional(),
+ keys: z.string(),
+ }),
+};
+
+export const scrollToTextActionSchema: ActionSchema = {
+ name: 'scroll_to_text',
+ description:
+ 'If you dont find something which you want to interact with, scroll to it',
+ schema: z.object({
+ desc: z.string().optional(),
+ text: z.string(),
+ }),
+};
+
+export const getDropdownOptionsActionSchema: ActionSchema = {
+ name: 'get_dropdown_options',
+ description: 'Get all options from a native dropdown',
+ schema: z.object({
+ index: z.number(),
+ }),
+};
+
+export const selectDropdownOptionActionSchema: ActionSchema = {
+ name: 'select_dropdown_option',
+ description:
+ 'Select dropdown option for interactive element index by the text of the option you want to select',
+ schema: z.object({
+ index: z.number(),
+ text: z.string(),
+ }),
+};
diff --git a/packages/agent-infra/browser-use/src/agent/agents/base.ts b/packages/agent-infra/browser-use/src/agent/agents/base.ts
new file mode 100644
index 000000000..d3d98a0a3
--- /dev/null
+++ b/packages/agent-infra/browser-use/src/agent/agents/base.ts
@@ -0,0 +1,243 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/src/background/agent/agents/base.ts
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ */
+import type { z } from 'zod';
+import { jsonrepair } from 'jsonrepair';
+import type {
+ BaseChatModel,
+ BaseChatModelCallOptions,
+} from '@langchain/core/language_models/chat_models';
+import type { AgentContext, AgentOutput } from '../types';
+import type { BasePrompt } from '../prompts/base';
+import {
+ type BaseMessage,
+ AIMessage,
+ ToolMessage,
+} from '@langchain/core/messages';
+import { createLogger } from '../../utils';
+import type { Action } from '../actions/builder';
+
+const logger = createLogger('agent');
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type CallOptions = BaseChatModelCallOptions;
+
+// Update options to use Zod schema
+export interface BaseAgentOptions {
+ chatLLM: BaseChatModel;
+ context: AgentContext;
+ prompt: BasePrompt;
+}
+export interface ExtraAgentOptions {
+ id?: string;
+ toolCallingMethod?: string;
+ callOptions?: Partial;
+}
+
+const THINK_TAGS = /[\s\S]*?<\/think>/;
+
+/**
+ * Base class for all agents
+ * @param T - The Zod schema for the model output
+ * @param M - The type of the result field of the agent output
+ */
+export abstract class BaseAgent {
+ protected id: string;
+ protected chatLLM: BaseChatModel;
+ protected prompt: BasePrompt;
+ protected context: AgentContext;
+ protected actions: Record = {};
+ protected modelOutputSchema: T;
+ protected toolCallingMethod: string | null;
+ protected chatModelLibrary: string;
+ protected modelName: string;
+ protected withStructuredOutput: boolean;
+ protected callOptions?: CallOptions;
+ protected modelOutputToolName: string;
+ declare ModelOutput: z.infer;
+
+ constructor(
+ modelOutputSchema: T,
+ options: BaseAgentOptions,
+ extraOptions?: Partial,
+ ) {
+ // base options
+ this.modelOutputSchema = modelOutputSchema;
+ this.chatLLM = options.chatLLM;
+ this.prompt = options.prompt;
+ this.context = options.context;
+ this.chatModelLibrary = this.chatLLM.constructor.name;
+ this.modelName = this.setModelNames();
+ this.withStructuredOutput = this.setWithStructuredOutput();
+ // extra options
+ this.id = extraOptions?.id || 'agent';
+ this.toolCallingMethod = this.setToolCallingMethod(
+ extraOptions?.toolCallingMethod,
+ );
+ this.callOptions = extraOptions?.callOptions;
+ this.modelOutputToolName = `${this.id}_output`;
+ }
+
+ // Set the model name
+ private setModelNames(): string {
+ if ('model_name' in this.chatLLM) {
+ return this.chatLLM.model_name as string;
+ }
+ if ('model' in this.chatLLM) {
+ return this.chatLLM.model as string;
+ }
+ return 'Unknown';
+ }
+
+ // Set the tool calling method
+ private setToolCallingMethod(toolCallingMethod?: string): string | null {
+ if (toolCallingMethod === 'auto') {
+ switch (this.chatModelLibrary) {
+ case 'ChatGoogleGenerativeAI':
+ return null;
+ case 'ChatOpenAI':
+ case 'AzureChatOpenAI':
+ return 'function_calling';
+ default:
+ return null;
+ }
+ }
+ return toolCallingMethod || null;
+ }
+
+ // Set whether to use structured output based on the model name
+ private setWithStructuredOutput(): boolean {
+ if (
+ this.modelName === 'deepseek-reasoner' ||
+ this.modelName === 'deepseek-r1' ||
+ this.modelName.includes('claude')
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ // Remove think tags from the model output
+ protected removeThinkTags(text: string): string {
+ return text.replace(THINK_TAGS, '');
+ }
+
+ async invoke(inputMessages: BaseMessage[]): Promise {
+ // Use structured output
+ if (this.withStructuredOutput) {
+ const structuredLlm = this.chatLLM.withStructuredOutput(
+ this.modelOutputSchema,
+ {
+ includeRaw: true,
+ name: this.modelOutputToolName,
+ strict: true,
+ },
+ );
+
+ const response = await structuredLlm.invoke(inputMessages, {
+ ...this.callOptions,
+ });
+ if (response.parsed) {
+ return response.parsed;
+ }
+ throw new Error('Could not parse response');
+ }
+
+ // Without structured output support, need to extract JSON from model output manually
+ const response = await this.chatLLM.invoke(inputMessages, {
+ ...this.callOptions,
+ });
+ if (typeof response.content === 'string') {
+ response.content = this.removeThinkTags(response.content);
+ try {
+ const extractedJson = this.extractJsonFromModelOutput(response.content);
+ const parsed = this.validateModelOutput(extractedJson);
+ if (parsed) {
+ return parsed;
+ }
+ } catch (error) {
+ logger.error('Could not parse response', response);
+ throw new Error('Could not parse response');
+ }
+ }
+ throw new Error('Could not parse response');
+ }
+
+ // Execute the agent and return the result
+ abstract execute(): Promise>;
+
+ // Helper method to validate metadata
+ protected validateModelOutput(
+ data: unknown,
+ ): this['ModelOutput'] | undefined {
+ if (!this.modelOutputSchema || !data) return undefined;
+ return this.modelOutputSchema.parse(data);
+ }
+
+ // Add the model output to the memory
+ protected addModelOutputToMemory(modelOutput: this['ModelOutput']): void {
+ const messageManager = this.context.messageManager;
+ const toolCallId = String(messageManager.nextToolId());
+ const toolCalls = [
+ {
+ name: this.modelOutputToolName,
+ args: modelOutput,
+ id: toolCallId,
+ type: 'tool_call' as const,
+ },
+ ];
+
+ const toolCallMessage = new AIMessage({
+ content: 'tool call',
+ tool_calls: toolCalls,
+ });
+ messageManager.addMessageWithTokens(toolCallMessage);
+
+ const toolMessage = new ToolMessage({
+ content: 'tool call response placeholder',
+ tool_call_id: toolCallId,
+ });
+ messageManager.addMessageWithTokens(toolMessage);
+ }
+
+ /**
+ * Extract JSON from raw string model output, handling both plain JSON and code-block-wrapped JSON.
+ *
+ * some models not supporting tool calls well like deepseek-reasoner, so we need to extract the JSON from the output
+ * @param content - The content of the model output
+ * @returns The JSON object
+ */
+ protected extractJsonFromModelOutput(content: string): unknown {
+ try {
+ let cleanedContent = content;
+ // If content is wrapped in code blocks, extract just the JSON part
+ if (content.includes('```')) {
+ // Find the JSON content between code blocks
+ cleanedContent = cleanedContent.split('```')[1];
+ // Remove language identifier if present (e.g., 'json\n')
+ if (cleanedContent.includes('\n')) {
+ cleanedContent = cleanedContent.split('\n', 2)[1];
+ }
+ } else {
+ const jsonRegex = /(\{(?:[^{}]|(?:\{(?:[^{}]|(?:\{[^{}]*\}))*\}))*\})/;
+ const match = cleanedContent.match(jsonRegex);
+ if (match && match[1]) {
+ cleanedContent = match[1];
+ }
+ }
+
+ cleanedContent = jsonrepair(cleanedContent);
+
+ // Parse the cleaned content
+ return JSON.parse(cleanedContent);
+ } catch (e) {
+ logger.warning(`Failed to parse model output: ${content} ${e}`);
+ throw new Error('Could not parse response.');
+ }
+ }
+}
diff --git a/packages/agent-infra/browser-use/src/agent/agents/errors.ts b/packages/agent-infra/browser-use/src/agent/agents/errors.ts
new file mode 100644
index 000000000..00159679c
--- /dev/null
+++ b/packages/agent-infra/browser-use/src/agent/agents/errors.ts
@@ -0,0 +1,39 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/src/background/agent/agents/errors.ts
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ */
+
+/**
+ * Custom error class for chat model authentication errors
+ */
+export class ChatModelAuthError extends Error {
+ /**
+ * Creates a new ChatModelAuthError
+ *
+ * @param message - The error message
+ * @param cause - The original error that caused this error
+ */
+ constructor(
+ message: string,
+ public readonly cause?: unknown,
+ ) {
+ super(message);
+ this.name = 'ChatModelAuthError';
+
+ // Maintains proper stack trace for where our error was thrown
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, ChatModelAuthError);
+ }
+ }
+
+ /**
+ * Returns a string representation of the error
+ */
+ toString(): string {
+ return `${this.name}: ${this.message}${this.cause ? ` (Caused by: ${this.cause})` : ''}`;
+ }
+}
diff --git a/packages/agent-infra/browser-use/src/agent/agents/navigator.ts b/packages/agent-infra/browser-use/src/agent/agents/navigator.ts
new file mode 100644
index 000000000..68ce80290
--- /dev/null
+++ b/packages/agent-infra/browser-use/src/agent/agents/navigator.ts
@@ -0,0 +1,360 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/src/background/agent/agents/navigator.ts
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ */
+import { z } from 'zod';
+import {
+ BaseAgent,
+ type BaseAgentOptions,
+ type ExtraAgentOptions,
+} from './base';
+import { ActionResult, type AgentOutput } from '../types';
+import type { Action } from '../actions/builder';
+import { buildDynamicActionSchema } from '../actions/builder';
+import { agentBrainSchema } from '../types';
+import { type BaseMessage, HumanMessage } from '@langchain/core/messages';
+import { Actors, ExecutionState } from '../event/types';
+import { isAuthenticationError, createLogger } from '../../utils';
+import { ChatModelAuthError } from './errors';
+import { jsonNavigatorOutputSchema } from '../actions/json_schema';
+import { geminiNavigatorOutputSchema } from '../actions/json_gemini';
+import { jsonrepair } from 'jsonrepair';
+const logger = createLogger('NavigatorAgent');
+
+export class NavigatorActionRegistry {
+ private actions: Record = {};
+
+ constructor(actions: Action[]) {
+ for (const action of actions) {
+ this.registerAction(action);
+ }
+ }
+
+ registerAction(action: Action): void {
+ this.actions[action.name()] = action;
+ }
+
+ unregisterAction(name: string): void {
+ delete this.actions[name];
+ }
+
+ getAction(name: string): Action | undefined {
+ return this.actions[name];
+ }
+
+ setupModelOutputSchema(): z.ZodType {
+ const actionSchema = buildDynamicActionSchema(Object.values(this.actions));
+ return z.object({
+ current_state: agentBrainSchema,
+ action: z.array(actionSchema),
+ });
+ }
+}
+
+export interface NavigatorResult {
+ done: boolean;
+}
+
+export class NavigatorAgent extends BaseAgent {
+ private actionRegistry: NavigatorActionRegistry;
+
+ constructor(
+ actionRegistry: NavigatorActionRegistry,
+ options: BaseAgentOptions,
+ extraOptions?: Partial,
+ ) {
+ super(actionRegistry.setupModelOutputSchema(), options, {
+ ...extraOptions,
+ id: 'navigator',
+ });
+
+ this.actionRegistry = actionRegistry;
+ }
+
+ async invoke(inputMessages: BaseMessage[]): Promise {
+ logger.info('invoke this.chatLLM', this.withStructuredOutput);
+ // Use structured output
+ if (this.withStructuredOutput) {
+ // For Google Generative AI, we need to use the modelOutputSchema directly
+ // but make sure it doesn't have any 'default' properties that cause issues
+
+ const schema =
+ this.chatModelLibrary === 'ChatGoogleGenerativeAI'
+ ? geminiNavigatorOutputSchema
+ : jsonNavigatorOutputSchema;
+
+ // TODO: don't know why zod can not generate the same schema. Use the json schema exported from browser-use as a workaround for now, need to fix it
+ const structuredLlm = this.chatLLM.withStructuredOutput(schema, {
+ includeRaw: true,
+ });
+
+ console.log('structuredLlm', structuredLlm);
+
+ const response = await structuredLlm.invoke(inputMessages, {
+ ...this.callOptions,
+ });
+ logger.info('invoke structuredLlm response', response);
+
+ if (response.parsed) {
+ return response.parsed;
+ } else {
+ return response.raw;
+ }
+ // throw new Error('Could not parse response');
+ }
+
+ // Without structured output support, need to extract JSON from model output manually
+ const response = await this.chatLLM.invoke(inputMessages, {
+ ...this.callOptions,
+ });
+ logger.info('invoke response', response);
+ if (typeof response.content === 'string') {
+ response.content = this.removeThinkTags(response.content);
+ try {
+ const extractedJson = this.extractJsonFromModelOutput(response.content);
+ const parsed = this.validateModelOutput(extractedJson);
+ if (parsed) {
+ return parsed;
+ }
+ } catch (error) {
+ logger.error('Could not parse response', response);
+ throw new Error('Could not parse response');
+ }
+ }
+ throw new Error('Could not parse response');
+ }
+
+ async execute(): Promise> {
+ const agentOutput: AgentOutput = {
+ id: this.id,
+ };
+
+ let cancelled = false;
+
+ try {
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.STEP_START,
+ 'Navigating...',
+ );
+
+ const messageManager = this.context.messageManager;
+ // add the browser state message
+ await this.addStateMessageToMemory();
+ // check if the task is paused or stopped
+ if (this.context.paused || this.context.stopped) {
+ cancelled = true;
+ return agentOutput;
+ }
+
+ // call the model to get the actions to take
+ const inputMessages = messageManager.getMessages();
+ const modelOutput = await this.invoke(inputMessages);
+ logger.info('modelOutput', modelOutput);
+
+ // check if the task is paused or stopped
+ if (this.context.paused || this.context.stopped) {
+ cancelled = true;
+ return agentOutput;
+ }
+ // remove the last state message from memory before adding the model output
+ this.removeLastStateMessageFromMemory();
+ this.addModelOutputToMemory(modelOutput);
+
+ logger.info('modelOutput.action', modelOutput.action);
+
+ // take the actions
+ const actionResults = await this.doMultiAction(modelOutput);
+ logger.info('actionResults', actionResults);
+ this.context.actionResults = actionResults;
+
+ // check if the task is paused or stopped
+ if (this.context.paused || this.context.stopped) {
+ cancelled = true;
+ return agentOutput;
+ }
+ // emit event
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.STEP_OK,
+ 'Navigation done',
+ );
+ let done = false;
+ if (
+ actionResults.length > 0 &&
+ actionResults[actionResults.length - 1].isDone
+ ) {
+ done = true;
+ }
+ agentOutput.result = { done };
+ return agentOutput;
+ } catch (error) {
+ logger.error('execute error', error);
+ this.removeLastStateMessageFromMemory();
+ // Check if this is an authentication error
+ if (isAuthenticationError(error)) {
+ throw new ChatModelAuthError(
+ 'Navigator API Authentication failed. Please verify your API key',
+ error,
+ );
+ }
+
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ const errorString = `Navigation failed: ${errorMessage}`;
+ logger.error(errorString);
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.STEP_FAIL,
+ errorString,
+ );
+ agentOutput.error = errorMessage;
+ return agentOutput;
+ } finally {
+ // if the task is cancelled, remove the last state message from memory and emit event
+ if (cancelled) {
+ this.removeLastStateMessageFromMemory();
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.STEP_CANCEL,
+ 'Navigation cancelled',
+ );
+ }
+ }
+ }
+
+ /**
+ * Add the state message to the memory
+ */
+ public async addStateMessageToMemory() {
+ if (this.context.stateMessageAdded) {
+ return;
+ }
+
+ const messageManager = this.context.messageManager;
+ const options = this.context.options;
+ // Handle results that should be included in memory
+ if (this.context.actionResults.length > 0) {
+ let index = 0;
+ for (const r of this.context.actionResults) {
+ if (r.includeInMemory) {
+ if (r.extractedContent) {
+ const msg = new HumanMessage(
+ `Action result: ${r.extractedContent}`,
+ );
+ // logger.info('Adding action result to memory', msg.content);
+ messageManager.addMessageWithTokens(msg);
+ }
+ if (r.error) {
+ const msg = new HumanMessage(
+ `Action error: ${r.error.toString().slice(-options.maxErrorLength)}`,
+ );
+ logger.info('Adding action error to memory', msg.content);
+ messageManager.addMessageWithTokens(msg);
+ }
+ // reset this action result to empty, we dont want to add it again in the state message
+ this.context.actionResults[index] = new ActionResult();
+ }
+ index++;
+ }
+ }
+
+ const state = await this.prompt.getUserMessage(this.context);
+ messageManager.addStateMessage(state);
+ this.context.stateMessageAdded = true;
+ }
+
+ /**
+ * Remove the last state message from the memory
+ */
+ protected async removeLastStateMessageFromMemory() {
+ if (!this.context.stateMessageAdded) return;
+ const messageManager = this.context.messageManager;
+ messageManager.removeLastStateMessage();
+ this.context.stateMessageAdded = false;
+ }
+
+ private async doMultiAction(
+ response: this['ModelOutput'],
+ ): Promise {
+ const results: ActionResult[] = [];
+ let errCount = 0;
+
+ logger.info('doMultiAction_Actions', response.action);
+ // sometimes response.action is a string, but not an array as expected, so we need to parse it as an array
+ let actions: Record[] = [];
+ if (Array.isArray(response.action)) {
+ // if the item is null, skip it
+ actions = response.action.filter((item: unknown) => item !== null);
+ if (actions.length === 0) {
+ logger.warning('No valid actions found', response.action);
+ }
+ } else if (typeof response.action === 'string') {
+ try {
+ logger.warning('Unexpected action format', response.action);
+ const repaired = jsonrepair(response.action);
+ // try to parse the action as an JSON object
+ actions = JSON.parse(repaired);
+ } catch (error) {
+ logger.error('Invalid action format', response.action);
+ throw new Error('Invalid action output format');
+ }
+ } else {
+ // if the action is neither an array nor a string, it should be an object
+ actions = [response.action];
+ }
+
+ for (const action of actions) {
+ const actionName = Object.keys(action)[0];
+ const actionArgs = action[actionName];
+ try {
+ // check if the task is paused or stopped
+ if (this.context.paused || this.context.stopped) {
+ return results;
+ }
+
+ const result = await this.actionRegistry
+ .getAction(actionName)
+ ?.call(actionArgs);
+ if (result === undefined) {
+ throw new Error(
+ `Action ${actionName} not exists or returned undefined`,
+ );
+ }
+ results.push(result);
+ // check if the task is paused or stopped
+ if (this.context.paused || this.context.stopped) {
+ return results;
+ }
+ // TODO: wait for 1 second for now, need to optimize this to avoid unnecessary waiting
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ logger.error('doAction error', errorMessage);
+ // unexpected error, emit event
+ this.context.emitEvent(
+ Actors.NAVIGATOR,
+ ExecutionState.ACT_FAIL,
+ errorMessage,
+ );
+ errCount++;
+ if (errCount > 3) {
+ throw new Error('Too many errors in actions');
+ }
+ results.push(
+ new ActionResult({
+ error: errorMessage,
+ isDone: false,
+ includeInMemory: true,
+ }),
+ );
+ }
+ }
+ return results;
+ }
+}
diff --git a/packages/agent-infra/browser-use/src/agent/agents/planner.ts b/packages/agent-infra/browser-use/src/agent/agents/planner.ts
new file mode 100644
index 000000000..eb1836f04
--- /dev/null
+++ b/packages/agent-infra/browser-use/src/agent/agents/planner.ts
@@ -0,0 +1,117 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/src/background/agent/agents/planner.ts
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ */
+import {
+ BaseAgent,
+ type BaseAgentOptions,
+ type ExtraAgentOptions,
+} from './base';
+import { createLogger, isAuthenticationError } from '../../utils';
+import { z } from 'zod';
+import type { AgentOutput } from '../types';
+import { HumanMessage } from '@langchain/core/messages';
+import { Actors, ExecutionState } from '../event/types';
+import { ChatModelAuthError } from './errors';
+const logger = createLogger('PlannerAgent');
+
+// Define Zod schema for planner output
+export const plannerOutputSchema = z.object({
+ observation: z.string(),
+ challenges: z.string(),
+ done: z.boolean(),
+ next_steps: z.string(),
+ reasoning: z.string(),
+ web_task: z.boolean(),
+});
+
+export type PlannerOutput = z.infer;
+
+export class PlannerAgent extends BaseAgent<
+ typeof plannerOutputSchema,
+ PlannerOutput
+> {
+ constructor(
+ options: BaseAgentOptions,
+ extraOptions?: Partial,
+ ) {
+ super(plannerOutputSchema, options, { ...extraOptions, id: 'planner' });
+ }
+
+ async execute(): Promise> {
+ try {
+ this.context.emitEvent(
+ Actors.PLANNER,
+ ExecutionState.STEP_START,
+ 'Planning...',
+ );
+ // get all messages from the message manager, state message should be the last one
+ const messages = this.context.messageManager.getMessages();
+ // Use full message history except the first one
+ const plannerMessages = [
+ this.prompt.getSystemMessage(),
+ ...messages.slice(1),
+ ];
+
+ // Remove images from last message if vision is not enabled for planner but vision is enabled
+ if (
+ !this.context.options.useVisionForPlanner &&
+ this.context.options.useVision
+ ) {
+ const lastStateMessage = plannerMessages[plannerMessages.length - 1];
+ let newMsg = '';
+
+ if (Array.isArray(lastStateMessage.content)) {
+ for (const msg of lastStateMessage.content) {
+ if (msg.type === 'text') {
+ newMsg += msg.text;
+ }
+ // Skip image_url messages
+ }
+ } else {
+ newMsg = lastStateMessage.content;
+ }
+
+ plannerMessages[plannerMessages.length - 1] = new HumanMessage(newMsg);
+ }
+
+ const modelOutput = await this.invoke(plannerMessages);
+ if (!modelOutput) {
+ throw new Error('Failed to validate planner output');
+ }
+ this.context.emitEvent(
+ Actors.PLANNER,
+ ExecutionState.STEP_OK,
+ modelOutput.next_steps,
+ );
+
+ return {
+ id: this.id,
+ result: modelOutput,
+ };
+ } catch (error) {
+ // Check if this is an authentication error
+ if (isAuthenticationError(error)) {
+ throw new ChatModelAuthError(
+ 'Planner API Authentication failed. Please verify your API key',
+ error,
+ );
+ }
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ this.context.emitEvent(
+ Actors.PLANNER,
+ ExecutionState.STEP_FAIL,
+ `Planning failed: ${errorMessage}`,
+ );
+ return {
+ id: this.id,
+ error: errorMessage,
+ };
+ }
+ }
+}
diff --git a/packages/agent-infra/browser-use/src/agent/agents/validator.ts b/packages/agent-infra/browser-use/src/agent/agents/validator.ts
new file mode 100644
index 000000000..331fade2c
--- /dev/null
+++ b/packages/agent-infra/browser-use/src/agent/agents/validator.ts
@@ -0,0 +1,124 @@
+/**
+ * The following code is modified based on
+ * https://github.com/nanobrowser/nanobrowser/blob/master/chrome-extension/src/background/agent/agents/validator.ts
+ *
+ * Apache-2.0 License
+ * Copyright (c) 2024 alexchenzl
+ * https://github.com/nanobrowser/nanobrowser/blob/master/LICENSE
+ */
+import {
+ BaseAgent,
+ type BaseAgentOptions,
+ type ExtraAgentOptions,
+} from './base';
+import { z } from 'zod';
+import { ActionResult, type AgentOutput } from '../types';
+import { Actors, ExecutionState } from '../event/types';
+import { HumanMessage } from '@langchain/core/messages';
+import { isAuthenticationError, createLogger } from '../../utils';
+import { ChatModelAuthError } from './errors';
+const logger = createLogger('ValidatorAgent');
+
+// Define Zod schema for validator output
+export const validatorOutputSchema = z.object({
+ is_valid: z.boolean(), // indicates if the output is correct
+ reason: z.string(), // explains why it is valid or not
+ answer: z.string(), // the final answer to the task if it is valid
+});
+
+export type ValidatorOutput = z.infer;
+
+export class ValidatorAgent extends BaseAgent<
+ typeof validatorOutputSchema,
+ ValidatorOutput
+> {
+ // sometimes we need to validate the output against both the current browser state and the plan
+ private plan: string | null = null;
+ constructor(
+ options: BaseAgentOptions,
+ extraOptions?: Partial,
+ ) {
+ super(validatorOutputSchema, options, { ...extraOptions, id: 'validator' });
+ }
+
+ /**
+ * Set the plan for the validator agent
+ * @param plan - The plan to set
+ */
+ setPlan(plan: string | null): void {
+ this.plan = plan;
+ }
+
+ /**
+ * Executes the validator agent
+ * @returns AgentOutput
+ */
+ async execute(): Promise