Merge branch 'fix/update-contact-link' into deploy/dev

This commit is contained in:
NFish 2025-03-07 14:56:25 +08:00
commit 24c9954651
57 changed files with 1106 additions and 473 deletions

153
CONTRIBUTING_TW.md Normal file
View File

@ -0,0 +1,153 @@
# 貢獻指南
您想為 Dify 做出貢獻 - 這太棒了,我們迫不及待地想看看您的成果。作為一家人力和資金有限的初創公司,我們有宏大的抱負,希望設計出最直觀的工作流程來構建和管理 LLM 應用程式。來自社群的任何幫助都非常珍貴,真的。
鑑於我們的現狀,我們需要靈活且快速地發展,但同時也希望確保像您這樣的貢獻者能夠獲得盡可能順暢的貢獻體驗。我們編寫了這份貢獻指南,目的是幫助您熟悉代碼庫以及我們如何與貢獻者合作,讓您可以更快地進入有趣的部分。
這份指南,就像 Dify 本身一樣,是不斷發展的。如果有時它落後於實際項目,我們非常感謝您的理解,也歡迎任何改進的反饋。
關於授權,請花一分鐘閱讀我們簡短的[授權和貢獻者協議](./LICENSE)。社群也遵守[行為準則](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md)。
## 在開始之前
[尋找](https://github.com/langgenius/dify/issues?q=is:issue+is:open)現有的 issue或[創建](https://github.com/langgenius/dify/issues/new/choose)一個新的。我們將 issues 分為 2 種類型:
### 功能請求
- 如果您要開啟新的功能請求,我們希望您能解釋所提議的功能要達成什麼目標,並且盡可能包含更多的相關背景資訊。[@perzeusss](https://github.com/perzeuss) 已經製作了一個實用的[功能請求輔助工具](https://udify.app/chat/MK2kVSnw1gakVwMX),能幫助您草擬您的需求。歡迎試用。
- 如果您想從現有問題中選擇一個來處理,只需在其下方留言表示即可。
相關方向的團隊成員會加入討論。如果一切順利,他們會同意您開始編寫代碼。我們要求您在得到許可前先不要開始處理該功能,以免我們提出變更時您的工作成果被浪費。
根據所提議功能的領域不同,您可能會與不同的團隊成員討論。以下是目前每位團隊成員所負責的領域概述:
| 成員 | 負責領域 |
| --------------------------------------------------------------------------------------- | ------------------------------ |
| [@yeuoly](https://github.com/Yeuoly) | 設計 Agents 架構 |
| [@jyong](https://github.com/JohnJyong) | RAG 管道設計 |
| [@GarfieldDai](https://github.com/GarfieldDai) | 建構工作流程編排 |
| [@iamjoel](https://github.com/iamjoel) & [@zxhlyh](https://github.com/zxhlyh) | 打造易用的前端界面 |
| [@guchenhe](https://github.com/guchenhe) & [@crazywoola](https://github.com/crazywoola) | 開發者體驗,各類問題的聯絡窗口 |
| [@takatost](https://github.com/takatost) | 整體產品方向與架構 |
我們如何排定優先順序:
| 功能類型 | 優先級 |
| ------------------------------------------------------------------------------------------------------- | -------- |
| 被團隊成員標記為高優先級的功能 | 高優先級 |
| 來自我們[社群回饋版](https://github.com/langgenius/dify/discussions/categories/feedbacks)的熱門功能請求 | 中優先級 |
| 非核心功能和次要增強 | 低優先級 |
| 有價值但非急迫的功能 | 未來功能 |
### 其他事項 (例如錯誤回報、效能優化、錯字更正)
- 可以直接開始編寫程式碼。
我們如何排定優先順序:
| 問題類型 | 優先級 |
| ----------------------------------------------------- | -------- |
| 核心功能的錯誤 (無法登入、應用程式無法運行、安全漏洞) | 重要 |
| 非關鍵性錯誤、效能提升 | 中優先級 |
| 小修正 (錯字、令人困惑但仍可運作的使用者界面) | 低優先級 |
## 安裝
以下是設置 Dify 開發環境的步驟:
### 1. 分叉此存儲庫
### 2. 複製代碼庫
從您的終端機複製分叉的代碼庫:
```shell
git clone git@github.com:<github_username>/dify.git
```
- [Docker](https://www.docker.com/)
- [Docker Compose](https://docs.docker.com/compose/install/)
- [Node.js v18.x (LTS)](http://nodejs.org)
- [pnpm](https://pnpm.io/)
- [Python](https://www.python.org/) version 3.11.x or 3.12.x
### 4. 安裝
Dify 由後端和前端組成。透過 `cd api/` 導航至後端目錄,然後按照[後端 README](api/README.md)進行安裝。在另一個終端機視窗中,透過 `cd web/` 導航至前端目錄,然後按照[前端 README](web/README.md)進行安裝。
查閱[安裝常見問題](https://docs.dify.ai/learn-more/faq/install-faq)了解常見問題和故障排除步驟的列表。
### 5. 在瀏覽器中訪問 Dify
要驗證您的設置,請在瀏覽器中訪問 [http://localhost:3000](http://localhost:3000)(預設值,或您自行設定的 URL 和埠號)。現在您應該能看到 Dify 已啟動並運行。
## 開發
如果您要添加模型提供者,請參考[此指南](https://github.com/langgenius/dify/blob/main/api/core/model_runtime/README.md)。
如果您要為 Agent 或工作流程添加工具提供者,請參考[此指南](./api/core/tools/README.md)。
為了幫助您快速找到您的貢獻適合的位置,以下是 Dify 後端和前端的簡要註解大綱:
### 後端
Dify 的後端使用 Python 的 [Flask](https://flask.palletsprojects.com/en/3.0.x/) 框架編寫。它使用 [SQLAlchemy](https://www.sqlalchemy.org/) 作為 ORM 工具,使用 [Celery](https://docs.celeryq.dev/en/stable/getting-started/introduction.html) 進行任務佇列處理。授權邏輯則透過 Flask-login 實現。
```text
[api/]
├── constants // 整個專案中使用的常數與設定值
├── controllers // API 路由定義與請求處理邏輯
├── core // 核心應用服務、模型整合與工具實現
├── docker // Docker 容器化相關設定檔案
├── events // 事件處理與流程管理機制
├── extensions // 與第三方框架或平台的整合擴充功能
├── fields // 資料序列化與結構定義欄位
├── libs // 可重複使用的共用程式庫與輔助工具
├── migrations // 資料庫結構變更與遷移腳本
├── models // 資料庫模型與資料結構定義
├── services // 核心業務邏輯與功能實現
├── storage // 私鑰與敏感資訊儲存機制
├── tasks // 非同步任務與背景作業處理器
└── tests
```
### 前端
網站基於 [Next.js](https://nextjs.org/) 的 Typescript 樣板,並使用 [Tailwind CSS](https://tailwindcss.com/) 進行樣式設計。[React-i18next](https://react.i18next.com/) 用於國際化。
```text
[web/]
├── app // 頁面佈局與介面元件
│ ├── (commonLayout) // 應用程式共用佈局結構
│ ├── (shareLayout) // Token 會話專用共享佈局
│ ├── activate // 帳號啟用頁面
│ ├── components // 頁面與佈局共用元件
│ ├── install // 系統安裝頁面
│ ├── signin // 使用者登入頁面
│ └── styles // 全域共用樣式定義
├── assets // 靜態資源檔案庫
├── bin // 建構流程執行腳本
├── config // 系統可調整設定與選項
├── context // 應用程式狀態共享上下文
├── dictionaries // 多語系翻譯詞彙庫
├── docker // Docker 容器設定檔
├── hooks // 可重複使用的 React Hooks
├── i18n // 國際化與本地化設定
├── models // 資料結構與 API 回應模型
├── public // 靜態資源與網站圖標
├── service // API 操作介面定義
├── test // 測試用例與測試框架
├── types // TypeScript 型別定義
└── utils // 共用輔助功能函式庫
```
## 提交您的 PR
最後是時候向我們的存儲庫開啟拉取請求PR了。對於主要功能我們會先將它們合併到 `deploy/dev` 分支進行測試,然後才會進入 `main` 分支。如果您遇到合併衝突或不知道如何開啟拉取請求等問題,請查看 [GitHub 的拉取請求教學](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests)。
就是這樣!一旦您的 PR 被合併,您將作為貢獻者出現在我們的 [README](https://github.com/langgenius/dify/blob/main/README.md) 中。
## 獲取幫助
如果您在貢獻過程中遇到困難或有迫切的問題,只需通過相關的 GitHub issue 向我們提問,或加入我們的 [Discord](https://discord.gg/8Tpq4AcN9c) 進行快速交流。

258
README_TW.md Normal file
View File

@ -0,0 +1,258 @@
![cover-v5-optimized](https://github.com/langgenius/dify/assets/13230914/f9e19af5-61ba-4119-b926-d10c4c06ebab)
<p align="center">
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">介紹 Dify 工作流程檔案上傳功能:重現 Google NotebookLM Podcast</a>
</p>
<p align="center">
<a href="https://cloud.dify.ai">Dify 雲端服務</a> ·
<a href="https://docs.dify.ai/getting-started/install-self-hosted">自行託管</a> ·
<a href="https://docs.dify.ai">說明文件</a> ·
<a href="https://udify.app/chat/22L1zSxg6yW1cWQg">企業諮詢</a>
</p>
<p align="center">
<a href="https://dify.ai" target="_blank">
<img alt="Static Badge" src="https://img.shields.io/badge/Product-F04438"></a>
<a href="https://dify.ai/pricing" target="_blank">
<img alt="Static Badge" src="https://img.shields.io/badge/free-pricing?logo=free&color=%20%23155EEF&label=pricing&labelColor=%20%23528bff"></a>
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
alt="chat on Discord"></a>
<a href="https://reddit.com/r/difyai" target="_blank">
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
alt="join Reddit"></a>
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
alt="follow on X(Twitter)"></a>
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
alt="follow on LinkedIn"></a>
<a href="https://hub.docker.com/u/langgenius" target="_blank">
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langgenius/dify-web?labelColor=%20%23FDB062&color=%20%23f79009"></a>
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
<img alt="Commits last month" src="https://img.shields.io/github/commit-activity/m/langgenius/dify?labelColor=%20%2332b583&color=%20%2312b76a"></a>
<a href="https://github.com/langgenius/dify/" target="_blank">
<img alt="Issues closed" src="https://img.shields.io/github/issues-search?query=repo%3Alanggenius%2Fdify%20is%3Aclosed&label=issues%20closed&labelColor=%20%237d89b0&color=%20%235d6b98"></a>
<a href="https://github.com/langgenius/dify/discussions/" target="_blank">
<img alt="Discussion posts" src="https://img.shields.io/github/discussions/langgenius/dify?labelColor=%20%239b8afb&color=%20%237a5af8"></a>
</p>
<p align="center">
<a href="./README.md"><img alt="README in English" src="https://img.shields.io/badge/English-d9d9d9"></a>
<a href="./README_TW.md"><img alt="繁體中文文件" src="https://img.shields.io/badge/繁體中文-d9d9d9"></a>
<a href="./README_CN.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
<a href="./README_JA.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
<a href="./README_ES.md"><img alt="README en Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
<a href="./README_FR.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README_KL.md"><img alt="README tlhIngan Hol" src="https://img.shields.io/badge/Klingon-d9d9d9"></a>
<a href="./README_KR.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
<a href="./README_DE.md"><img alt="README in Deutsch" src="https://img.shields.io/badge/German-d9d9d9"></a>
</p>
Dify 是一個開源的 LLM 應用程式開發平台。其直觀的界面結合了智能代理工作流程、RAG 管道、代理功能、模型管理、可觀察性功能等,讓您能夠快速從原型進展到生產環境。
## 快速開始
> 安裝 Dify 之前,請確保您的機器符合以下最低系統要求:
>
> - CPU >= 2 核心
> - 記憶體 >= 4 GiB
</br>
啟動 Dify 伺服器最簡單的方式是透過 [docker compose](docker/docker-compose.yaml)。在使用以下命令運行 Dify 之前,請確保您的機器已安裝 [Docker](https://docs.docker.com/get-docker/) 和 [Docker Compose](https://docs.docker.com/compose/install/)
```bash
cd dify
cd docker
cp .env.example .env
docker compose up -d
```
運行後,您可以在瀏覽器中通過 [http://localhost/install](http://localhost/install) 訪問 Dify 儀表板並開始初始化過程。
### 尋求幫助
如果您在設置 Dify 時遇到問題,請參考我們的 [常見問題](https://docs.dify.ai/getting-started/install-self-hosted/faqs)。如果仍有疑問,請聯絡 [社區和我們](#community--contact)。
> 如果您想為 Dify 做出貢獻或進行額外開發,請參考我們的 [從原始碼部署指南](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code)
## 核心功能
**1. 工作流程**
在視覺化畫布上建立和測試強大的 AI 工作流程,利用以下所有功能及更多。
https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa
**2. 全面的模型支援**
無縫整合來自數十個推理提供商和自託管解決方案的數百個專有/開源 LLM涵蓋 GPT、Mistral、Llama3 和任何與 OpenAI API 兼容的模型。您可以在[此處](https://docs.dify.ai/getting-started/readme/model-providers)找到支援的模型提供商完整列表。
![providers-v5](https://github.com/langgenius/dify/assets/13230914/5a17bdbe-097a-4100-8363-40255b70f6e3)
**3. 提示詞 IDE**
直觀的界面,用於編寫提示詞、比較模型性能,以及為聊天型應用程式添加文字轉語音等額外功能。
**4. RAG 管道**
廣泛的 RAG 功能,涵蓋從文件擷取到檢索的全部流程,內建支援從 PDF、PPT 和其他常見文件格式提取文本。
**5. 代理功能**
您可以基於 LLM 函數調用或 ReAct 定義代理並為代理添加預構建或自定義工具。Dify 為 AI 代理提供 50 多種內建工具,如 Google 搜尋、DALL·E、Stable Diffusion 和 WolframAlpha。
**6. LLMOps**
監控並分析應用程式日誌和長期效能。您可以根據生產數據和標註持續改進提示詞、數據集和模型。
**7. 後端即服務**
Dify 的所有功能都提供相應的 API因此您可以輕鬆地將 Dify 整合到您自己的業務邏輯中。
## 功能比較
<table style="width: 100%;">
<tr>
<th align="center">功能</th>
<th align="center">Dify.AI</th>
<th align="center">LangChain</th>
<th align="center">Flowise</th>
<th align="center">OpenAI Assistants API</th>
</tr>
<tr>
<td align="center">程式設計方法</td>
<td align="center">API + 應用導向</td>
<td align="center">Python 代碼</td>
<td align="center">應用導向</td>
<td align="center">API 導向</td>
</tr>
<tr>
<td align="center">支援的 LLM 模型</td>
<td align="center">豐富多樣</td>
<td align="center">豐富多樣</td>
<td align="center">豐富多樣</td>
<td align="center">僅限 OpenAI</td>
</tr>
<tr>
<td align="center">RAG 引擎</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">代理功能</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">工作流程</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">可觀察性</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">企業級功能 (SSO/存取控制)</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">本地部署</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
</table>
## 使用 Dify
- **雲端服務 </br>**
我們提供 [Dify Cloud](https://dify.ai) 服務,任何人都可以零配置嘗試。它提供與自部署版本相同的所有功能,並在沙盒計劃中包含 200 次免費 GPT-4 調用。
- **自託管 Dify 社區版</br>**
使用這份[快速指南](#快速開始)在您的環境中快速運行 Dify。
使用我們的[文檔](https://docs.dify.ai)獲取更多參考和深入指導。
- **企業/組織版 Dify</br>**
我們提供額外的企業中心功能。[通過這個聊天機器人記錄您的問題](https://udify.app/chat/22L1zSxg6yW1cWQg)或[發送電子郵件給我們](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry)討論企業需求。</br>
> 對於使用 AWS 的初創企業和小型企業,請查看 [AWS Marketplace 上的 Dify Premium](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6),並一鍵部署到您自己的 AWS VPC。這是一個經濟實惠的 AMI 產品,可選擇使用自定義徽標和品牌創建應用。
## 保持領先
在 GitHub 上為 Dify 加星,即時獲取新版本通知。
![star-us](https://github.com/langgenius/dify/assets/13230914/b823edc1-6388-4e25-ad45-2f6b187adbb4)
## 進階設定
如果您需要自定義配置,請參考我們的 [.env.example](docker/.env.example) 文件中的註釋,並在您的 `.env` 文件中更新相應的值。此外,根據您特定的部署環境和需求,您可能需要調整 `docker-compose.yaml` 文件本身,例如更改映像版本、端口映射或卷掛載。進行任何更改後,請重新運行 `docker-compose up -d`。您可以在[這裡](https://docs.dify.ai/getting-started/install-self-hosted/environments)找到可用環境變數的完整列表。
如果您想配置高可用性設置,社區貢獻的 [Helm Charts](https://helm.sh/) 和 YAML 文件允許在 Kubernetes 上部署 Dify。
- [由 @LeoQuote 提供的 Helm Chart](https://github.com/douban/charts/tree/master/charts/dify)
- [由 @BorisPolonsky 提供的 Helm Chart](https://github.com/BorisPolonsky/dify-helm)
- [由 @Winson-030 提供的 YAML 文件](https://github.com/Winson-030/dify-kubernetes)
### 使用 Terraform 進行部署
使用 [terraform](https://www.terraform.io/) 一鍵部署 Dify 到雲端平台
### Azure 全球
- [由 @nikawang 提供的 Azure Terraform](https://github.com/nikawang/dify-azure-terraform)
### Google Cloud
- [由 @sotazum 提供的 Google Cloud Terraform](https://github.com/DeNA/dify-google-cloud-terraform)
### 使用 AWS CDK 進行部署
使用 [CDK](https://aws.amazon.com/cdk/) 部署 Dify 到 AWS
### AWS
- [由 @KevinZhao 提供的 AWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
## 貢獻
對於想要貢獻程式碼的開發者,請參閱我們的[貢獻指南](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)。
同時,也請考慮透過在社群媒體和各種活動與會議上分享 Dify 來支持我們。
> 我們正在尋找貢獻者協助將 Dify 翻譯成中文和英文以外的語言。如果您有興趣幫忙,請查看 [i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) 獲取更多資訊,並在我們的 [Discord 社群伺服器](https://discord.gg/8Tpq4AcN9c) 的 `global-users` 頻道留言給我們。
## 社群與聯絡方式
- [Github Discussion](https://github.com/langgenius/dify/discussions):最適合分享反饋和提問。
- [GitHub Issues](https://github.com/langgenius/dify/issues):最適合報告使用 Dify.AI 時遇到的問題和提出功能建議。請參閱我們的[貢獻指南](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)。
- [Discord](https://discord.gg/FngNHpbcY7):最適合分享您的應用程式並與社群互動。
- [X(Twitter)](https://twitter.com/dify_ai):最適合分享您的應用程式並與社群互動。
**貢獻者**
<a href="https://github.com/langgenius/dify/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langgenius/dify" />
</a>
## 星星歷史
[![Star History Chart](https://api.star-history.com/svg?repos=langgenius/dify&type=Date)](https://star-history.com/#langgenius/dify&Date)
## 安全揭露
為保護您的隱私,請避免在 GitHub 上發布安全性問題。請將您的問題發送至 security@dify.ai我們將為您提供更詳細的答覆。
## 授權條款
本代碼庫採用 [Dify 開源授權](LICENSE),這基本上是 Apache 2.0 授權加上一些額外限制條款。

View File

@ -187,18 +187,30 @@ class ProviderConfiguration(BaseModel):
:return: :return:
""" """
# get provider # get provider
provider_record = ( model_provider_id = ModelProviderID(self.provider.provider)
db.session.query(Provider) if model_provider_id.is_langgenius():
.filter( provider_record = (
Provider.tenant_id == self.tenant_id, db.session.query(Provider)
Provider.provider_type == ProviderType.CUSTOM.value, .filter(
or_( Provider.tenant_id == self.tenant_id,
Provider.provider_name == ModelProviderID(self.provider.provider).plugin_name, Provider.provider_type == ProviderType.CUSTOM.value,
Provider.provider_name == self.provider.provider, or_(
), Provider.provider_name == model_provider_id.provider_name,
Provider.provider_name == self.provider.provider,
),
)
.first()
)
else:
provider_record = (
db.session.query(Provider)
.filter(
Provider.tenant_id == self.tenant_id,
Provider.provider_type == ProviderType.CUSTOM.value,
Provider.provider_name == self.provider.provider,
)
.first()
) )
.first()
)
# Get provider credential secret variables # Get provider credential secret variables
provider_credential_secret_variables = self.extract_secret_variables( provider_credential_secret_variables = self.extract_secret_variables(

View File

@ -164,6 +164,9 @@ class GenericProviderID:
self.organization, self.plugin_name, self.provider_name = value.split("/") self.organization, self.plugin_name, self.provider_name = value.split("/")
self.is_hardcoded = is_hardcoded self.is_hardcoded = is_hardcoded
def is_langgenius(self) -> bool:
return self.organization == "langgenius"
@property @property
def plugin_id(self) -> str: def plugin_id(self) -> str:
return f"{self.organization}/{self.plugin_name}" return f"{self.organization}/{self.plugin_name}"

View File

@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS {table_name} (
SQL_CREATE_INDEX = """ SQL_CREATE_INDEX = """
CREATE INDEX IF NOT EXISTS idx_docs_{table_name} ON {table_name}(text) CREATE INDEX IF NOT EXISTS idx_docs_{table_name} ON {table_name}(text)
INDEXTYPE IS CTXSYS.CONTEXT PARAMETERS INDEXTYPE IS CTXSYS.CONTEXT PARAMETERS
('FILTER CTXSYS.NULL_FILTER SECTION GROUP CTXSYS.HTML_SECTION_GROUP LEXER multilingual_lexer') ('FILTER CTXSYS.NULL_FILTER SECTION GROUP CTXSYS.HTML_SECTION_GROUP LEXER world_lexer')
""" """

View File

@ -51,8 +51,7 @@ from core.rag.retrieval.template_prompts import (
) )
from core.tools.utils.dataset_retriever.dataset_retriever_base_tool import DatasetRetrieverBaseTool from core.tools.utils.dataset_retriever.dataset_retriever_base_tool import DatasetRetrieverBaseTool
from extensions.ext_database import db from extensions.ext_database import db
from libs.json_in_md_parser import parse_and_check_json_markdown from models.dataset import ChildChunk, Dataset, DatasetQuery, DocumentSegment
from models.dataset import ChildChunk, Dataset, DatasetMetadata, DatasetQuery, DocumentSegment
from models.dataset import Document as DatasetDocument from models.dataset import Document as DatasetDocument
from services.external_knowledge_service import ExternalDatasetService from services.external_knowledge_service import ExternalDatasetService

View File

@ -120,6 +120,10 @@ class Response:
if disp_type == "attachment" or filename is not None: if disp_type == "attachment" or filename is not None:
return True return True
# For 'text/' types, only 'csv' should be downloaded as file
if content_type.startswith("text/") and "csv" not in content_type:
return False
# For application types, try to detect if it's a text-based format # For application types, try to detect if it's a text-based format
if content_type.startswith("application/"): if content_type.startswith("application/"):
# Common text-based application types # Common text-based application types

View File

@ -0,0 +1,64 @@
"""extend provider column
Revision ID: 5511c782ee4c
Revises: 4413929e1ec2
Create Date: 2025-03-07 03:15:05.364804
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5511c782ee4c'
down_revision = '4413929e1ec2'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tool_api_providers', schema=None) as batch_op:
batch_op.alter_column('name',
existing_type=sa.VARCHAR(length=40),
type_=sa.String(length=255),
existing_nullable=False)
with op.batch_alter_table('tool_model_invokes', schema=None) as batch_op:
batch_op.alter_column('provider',
existing_type=sa.VARCHAR(length=40),
type_=sa.String(length=255),
existing_nullable=False)
with op.batch_alter_table('tool_workflow_providers', schema=None) as batch_op:
batch_op.alter_column('name',
existing_type=sa.VARCHAR(length=40),
type_=sa.String(length=255),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tool_workflow_providers', schema=None) as batch_op:
batch_op.alter_column('name',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=40),
existing_nullable=False)
with op.batch_alter_table('tool_model_invokes', schema=None) as batch_op:
batch_op.alter_column('provider',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=40),
existing_nullable=False)
with op.batch_alter_table('tool_api_providers', schema=None) as batch_op:
batch_op.alter_column('name',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=40),
existing_nullable=False)
# ### end Alembic commands ###

View File

@ -64,7 +64,7 @@ class ApiToolProvider(Base):
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
# name of the api provider # name of the api provider
name = db.Column(db.String(40), nullable=False) name = db.Column(db.String(255), nullable=False)
# icon # icon
icon = db.Column(db.String(255), nullable=False) icon = db.Column(db.String(255), nullable=False)
# original schema # original schema
@ -143,7 +143,7 @@ class WorkflowToolProvider(Base):
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
# name of the workflow provider # name of the workflow provider
name: Mapped[str] = mapped_column(db.String(40), nullable=False) name: Mapped[str] = mapped_column(db.String(255), nullable=False)
# label of the workflow provider # label of the workflow provider
label: Mapped[str] = mapped_column(db.String(255), nullable=False, server_default="") label: Mapped[str] = mapped_column(db.String(255), nullable=False, server_default="")
# icon # icon
@ -205,7 +205,7 @@ class ToolModelInvoke(Base):
# tenant id # tenant id
tenant_id = db.Column(StringUUID, nullable=False) tenant_id = db.Column(StringUUID, nullable=False)
# provider # provider
provider = db.Column(db.String(40), nullable=False) provider = db.Column(db.String(255), nullable=False)
# type # type
tool_type = db.Column(db.String(40), nullable=False) tool_type = db.Column(db.String(40), nullable=False)
# tool name # tool name

View File

@ -19,6 +19,8 @@ acl Safe_ports port 488 # gss-http
acl Safe_ports port 591 # filemaker acl Safe_ports port 591 # filemaker
acl Safe_ports port 777 # multiling http acl Safe_ports port 777 # multiling http
acl CONNECT method CONNECT acl CONNECT method CONNECT
acl allowed_domains dstdomain .marketplace.dify.ai
http_access allow allowed_domains
http_access deny !Safe_ports http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports http_access deny CONNECT !SSL_ports
http_access allow localhost manager http_access allow localhost manager

View File

@ -5,6 +5,6 @@ create user dify identified by dify DEFAULT TABLESPACE users quota unlimited on
grant DB_DEVELOPER_ROLE to dify; grant DB_DEVELOPER_ROLE to dify;
BEGIN BEGIN
CTX_DDL.CREATE_PREFERENCE('dify.multilingual_lexer','CHINESE_VGRAM_LEXER'); CTX_DDL.CREATE_PREFERENCE('dify.world_lexer','WORLD_LEXER');
END; END;
/ /

View File

@ -186,7 +186,7 @@ const Chat: FC<ChatProps> = ({
if (chatContainer) { if (chatContainer) {
const setUserScrolled = () => { const setUserScrolled = () => {
if (chatContainer) if (chatContainer)
userScrolledRef.current = chatContainer.scrollHeight - chatContainer.scrollTop >= chatContainer.clientHeight + 300 userScrolledRef.current = chatContainer.scrollHeight - chatContainer.scrollTop > chatContainer.clientHeight
} }
chatContainer.addEventListener('scroll', setUserScrolled) chatContainer.addEventListener('scroll', setUserScrolled)
return () => chatContainer.removeEventListener('scroll', setUserScrolled) return () => chatContainer.removeEventListener('scroll', setUserScrolled)

View File

@ -17,15 +17,35 @@ export const processOpeningStatement = (openingStatement: string, inputs: Record
}) })
} }
export const processInputFileFromServer = (fileItem: Record<string, any>) => {
return {
type: fileItem.type,
transfer_method: fileItem.transfer_method,
url: fileItem.remote_url,
upload_file_id: fileItem.related_id,
}
}
export const getProcessedInputs = (inputs: Record<string, any>, inputsForm: InputForm[]) => { export const getProcessedInputs = (inputs: Record<string, any>, inputsForm: InputForm[]) => {
const processedInputs = { ...inputs } const processedInputs = { ...inputs }
inputsForm.forEach((item) => { inputsForm.forEach((item) => {
if (item.type === InputVarType.multiFiles && inputs[item.variable]) const inputValue = inputs[item.variable]
processedInputs[item.variable] = getProcessedFiles(inputs[item.variable]) if (!inputValue)
return
if (item.type === InputVarType.singleFile && inputs[item.variable]) if (item.type === InputVarType.singleFile) {
processedInputs[item.variable] = getProcessedFiles([inputs[item.variable]])[0] if ('transfer_method' in inputValue)
processedInputs[item.variable] = processInputFileFromServer(inputValue)
else
processedInputs[item.variable] = getProcessedFiles([inputValue])[0]
}
else if (item.type === InputVarType.multiFiles) {
if ('transfer_method' in inputValue[0])
processedInputs[item.variable] = inputValue.map(processInputFileFromServer)
else
processedInputs[item.variable] = getProcessedFiles(inputValue)
}
}) })
return processedInputs return processedInputs

View File

@ -238,7 +238,7 @@ const DatePicker = ({
</div> </div>
)} )}
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className={popupZIndexClassname}> <PortalToFollowElemContent className='z-50'>
<div className='w-[252px] mt-1 bg-components-panel-bg rounded-xl shadow-lg shadow-shadow-shadow-5 border-[0.5px] border-components-panel-border'> <div className='w-[252px] mt-1 bg-components-panel-bg rounded-xl shadow-lg shadow-shadow-shadow-5 border-[0.5px] border-components-panel-border'>
{/* Header */} {/* Header */}
{view === ViewType.date ? ( {view === ViewType.date ? (

View File

@ -142,7 +142,7 @@ const TimePicker = ({
</div> </div>
)} )}
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent> <PortalToFollowElemContent className='z-50'>
<div className='w-[252px] mt-1 bg-components-panel-bg rounded-xl shadow-lg shadow-shadow-shadow-5 border-[0.5px] border-components-panel-border'> <div className='w-[252px] mt-1 bg-components-panel-bg rounded-xl shadow-lg shadow-shadow-shadow-5 border-[0.5px] border-components-panel-border'>
{/* Header */} {/* Header */}
<Header /> <Header />

View File

@ -2,6 +2,10 @@ import React, { useEffect, useState } from 'react'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
import Checkbox from '@/app/components/base/checkbox'
import Select from '@/app/components/base/select'
import { useChatContext } from '@/app/components/base/chat/chat/context' import { useChatContext } from '@/app/components/base/chat/chat/context'
enum DATA_FORMAT { enum DATA_FORMAT {
@ -19,18 +23,13 @@ enum SUPPORTED_TYPES {
PASSWORD = 'password', PASSWORD = 'password',
EMAIL = 'email', EMAIL = 'email',
NUMBER = 'number', NUMBER = 'number',
DATE = 'date',
TIME = 'time',
DATETIME = 'datetime',
CHECKBOX = 'checkbox',
SELECT = 'select',
} }
const MarkdownForm = ({ node }: any) => { const MarkdownForm = ({ node }: any) => {
// const supportedTypes = ['text', 'password', 'email', 'number']
// <form data-format="text">
// <label for="username">Username:</label>
// <input type="text" name="username" />
// <label for="password">Password:</label>
// <input type="password" name="password" />
// <label for="content">Content:</label>
// <textarea name="content"></textarea>
// <button data-size="small" data-variant="primary">Login</button>
// </form>
const { onSend } = useChatContext() const { onSend } = useChatContext()
const [formValues, setFormValues] = useState<{ [key: string]: any }>({}) const [formValues, setFormValues] = useState<{ [key: string]: any }>({})
@ -90,6 +89,97 @@ const MarkdownForm = ({ node }: any) => {
) )
} }
if (child.tagName === SUPPORTED_TAGS.INPUT && Object.values(SUPPORTED_TYPES).includes(child.properties.type)) { if (child.tagName === SUPPORTED_TAGS.INPUT && Object.values(SUPPORTED_TYPES).includes(child.properties.type)) {
if (child.properties.type === SUPPORTED_TYPES.DATE || child.properties.type === SUPPORTED_TYPES.DATETIME) {
return (
<DatePicker
key={index}
value={formValues[child.properties.name]}
needTimePicker={child.properties.type === SUPPORTED_TYPES.DATETIME}
onChange={(date) => {
setFormValues(prevValues => ({
...prevValues,
[child.properties.name]: date,
}))
}}
onClear={() => {
setFormValues(prevValues => ({
...prevValues,
[child.properties.name]: undefined,
}))
}}
/>
)
}
if (child.properties.type === SUPPORTED_TYPES.TIME) {
return (
<TimePicker
key={index}
value={formValues[child.properties.name]}
onChange={(time) => {
setFormValues(prevValues => ({
...prevValues,
[child.properties.name]: time,
}))
}}
onClear={() => {
setFormValues(prevValues => ({
...prevValues,
[child.properties.name]: undefined,
}))
}}
/>
)
}
if (child.properties.type === SUPPORTED_TYPES.CHECKBOX) {
return (
<div className='mt-2 flex items-center h-6 space-x-2' key={index}>
<Checkbox
key={index}
checked={formValues[child.properties.name]}
onCheck={() => {
setFormValues(prevValues => ({
...prevValues,
[child.properties.name]: !prevValues[child.properties.name],
}))
}}
/>
<span>{child.properties.dataTip || child.properties['data-tip'] || ''}</span>
</div>
)
}
if (child.properties.type === SUPPORTED_TYPES.SELECT) {
return (
<Select
key={index}
allowSearch={false}
className="w-full"
items={(() => {
let options = child.properties.dataOptions || child.properties['data-options'] || []
if (typeof options === 'string') {
try {
options = JSON.parse(options)
}
catch (e) {
console.error('Failed to parse options:', e)
options = []
}
}
return options.map((option: string) => ({
name: option,
value: option,
}))
})()}
defaultValue={formValues[child.properties.name]}
onSelect={(item) => {
setFormValues(prevValues => ({
...prevValues,
[child.properties.name]: item.value,
}))
}}
/>
)
}
return ( return (
<Input <Input
key={index} key={index}

View File

@ -68,6 +68,7 @@ const preprocessLaTeX = (content: string) => {
return flow([ return flow([
(str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`), (str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/\\\[(.*?)\\\]/gs, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`), (str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`), (str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`),
])(content) ])(content)

View File

@ -16,7 +16,7 @@ import Checkbox from '@/app/components/base/checkbox'
import { bindTag, createTag, fetchTagList, unBindTag } from '@/service/tag' import { bindTag, createTag, fetchTagList, unBindTag } from '@/service/tag'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
interface TagSelectorProps { type TagSelectorProps = {
targetID: string targetID: string
isPopover?: boolean isPopover?: boolean
position?: 'bl' | 'br' position?: 'bl' | 'br'
@ -215,6 +215,7 @@ const TagSelector: FC<TagSelectorProps> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const tagList = useTagStore(s => s.tagList)
const setTagList = useTagStore(s => s.setTagList) const setTagList = useTagStore(s => s.setTagList)
const getTagList = async () => { const getTagList = async () => {
@ -224,9 +225,9 @@ const TagSelector: FC<TagSelectorProps> = ({
const triggerContent = useMemo(() => { const triggerContent = useMemo(() => {
if (selectedTags?.length) if (selectedTags?.length)
return selectedTags.map(tag => tag.name).join(', ') return selectedTags.filter(selectedTag => tagList.find(tag => tag.id === selectedTag.id)).map(tag => tag.name).join(', ')
return '' return ''
}, [selectedTags]) }, [selectedTags, tagList])
const Trigger = () => { const Trigger = () => {
return ( return (

View File

@ -4,10 +4,10 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useSWR from 'swr' import useSWR from 'swr'
import { import {
RiExternalLinkLine, RiArrowRightUpLine,
} from '@remixicon/react' } from '@remixicon/react'
import PlanComp from '../plan' import PlanComp from '../plan'
import { ReceiptList } from '../../base/icons/src/vender/line/financeAndECommerce' import Divider from '@/app/components/base/divider'
import { fetchBillingUrl } from '@/service/billing' import { fetchBillingUrl } from '@/service/billing'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
@ -25,13 +25,13 @@ const Billing: FC = () => {
<div> <div>
<PlanComp loc={'billing-page'} /> <PlanComp loc={'billing-page'} />
{enableBilling && isCurrentWorkspaceManager && billingUrl && ( {enableBilling && isCurrentWorkspaceManager && billingUrl && (
<a className='mt-5 flex px-6 justify-between h-12 items-center bg-gray-50 rounded-xl cursor-pointer' href={billingUrl} target='_blank' rel='noopener noreferrer'> <>
<div className='flex items-center'> <Divider className='my-4' />
<ReceiptList className='w-4 h-4 text-gray-700' /> <a className='flex items-center text-text-accent-light-mode-only system-xs-medium cursor-pointer' href={billingUrl} target='_blank' rel='noopener noreferrer'>
<div className='ml-2 text-sm font-normal text-gray-700'>{t('billing.viewBilling')}</div> <span className='pr-0.5'>{t('billing.viewBilling')}</span>
</div> <RiArrowRightUpLine className='w-4 h-4' />
<RiExternalLinkLine className='w-3 h-3' /> </a>
</a> </>
)} )}
</div> </div>
) )

View File

@ -6,7 +6,7 @@ export const NUM_INFINITE = 99999999
export const contractSales = 'contractSales' export const contractSales = 'contractSales'
export const unAvailable = 'unAvailable' export const unAvailable = 'unAvailable'
export const contactSalesUrl = 'https://vikgc6bnu1s.typeform.com/to/mowuXTQH' export const contactSalesUrl = 'https://vikgc6bnu1s.typeform.com/dify-business'
export const getStartedWithCommunityUrl = 'https://github.com/langgenius/dify' export const getStartedWithCommunityUrl = 'https://github.com/langgenius/dify'
export const getWithPremiumUrl = 'https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6' export const getWithPremiumUrl = 'https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6'

View File

@ -2,36 +2,21 @@
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
RiBook2Line,
RiBox3Line,
RiFileEditLine,
RiGroup3Line,
RiGroupLine,
RiSquareLine,
} from '@remixicon/react'
import { Plan, SelfHostedPlan } from '../type' import { Plan, SelfHostedPlan } from '../type'
import VectorSpaceInfo from '../usage-info/vector-space-info' import VectorSpaceInfo from '../usage-info/vector-space-info'
import AppsInfo from '../usage-info/apps-info' import AppsInfo from '../usage-info/apps-info'
import UpgradeBtn from '../upgrade-btn' import UpgradeBtn from '../upgrade-btn'
import { User01 } from '../../base/icons/src/vender/line/users'
import { MessageFastPlus } from '../../base/icons/src/vender/line/communication'
import { FileUpload } from '../../base/icons/src/vender/line/files'
import cn from '@/utils/classnames'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import UsageInfo from '@/app/components/billing/usage-info' import UsageInfo from '@/app/components/billing/usage-info'
const typeStyle = {
[Plan.sandbox]: {
textClassNames: 'text-gray-900',
bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #EAECF0',
},
[Plan.professional]: {
textClassNames: 'text-[#026AA2]',
bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #E0F2FE',
},
[Plan.team]: {
textClassNames: 'text-[#3538CD]',
bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #E0EAFF',
},
[SelfHostedPlan.enterprise]: {
textClassNames: 'text-[#DC6803]',
bg: 'linear-gradient(113deg, rgba(255, 255, 255, 0.51) 3.51%, rgba(255, 255, 255, 0.00) 111.71%), #FFEED3',
},
}
type Props = { type Props = {
loc: string loc: string
} }
@ -50,74 +35,62 @@ const PlanComp: FC<Props> = ({
total, total,
} = plan } = plan
const isInHeader = loc === 'header'
return ( return (
<div <div className='bg-background-section-burn rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off'>
className='rounded-xl border border-white select-none' <div className='p-6 pb-2'>
style={{ {plan.type === Plan.sandbox && (
background: typeStyle[type].bg, <RiBox3Line className='w-7 h-7 text-text-primary'/>
boxShadow: '5px 7px 12px 0px rgba(0, 0, 0, 0.06)',
}}
>
<div className='flex justify-between px-6 py-5 items-center'>
<div>
<div
className='leading-[18px] text-xs font-normal opacity-70'
style={{
color: 'rgba(0, 0, 0, 0.64)',
}}
>
{t('billing.currentPlan')}
</div>
<div className={cn(typeStyle[type].textClassNames, 'leading-[125%] text-lg font-semibold uppercase')}>
{t(`billing.plans.${type}.name`)}
</div>
</div>
{(!isInHeader || (isInHeader && type !== Plan.sandbox)) && (
<UpgradeBtn
className='flex-shrink-0'
isPlain={type !== Plan.sandbox}
loc={loc}
/>
)} )}
{plan.type === Plan.professional && (
<RiSquareLine className='w-7 h-7 rotate-90 text-util-colors-blue-brand-blue-brand-600'/>
)}
{plan.type === Plan.team && (
<RiGroup3Line className='w-7 h-7 text-util-colors-indigo-indigo-600'/>
)}
{(plan.type as any) === SelfHostedPlan.enterprise && (
<RiGroup3Line className='w-7 h-7 text-util-colors-indigo-indigo-600'/>
)}
<div className='mt-1 flex items-center'>
<div className='grow'>
<div className='mb-1 flex items-center gap-1'>
<div className='text-text-primary system-md-semibold-uppercase'>{t(`billing.plans.${type}.name`)}</div>
<div className='px-1 py-0.5 border border-divider-deep rounded-[5px] text-text-tertiary system-2xs-medium-uppercase'>{t('billing.currentPlan')}</div>
</div>
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
</div>
{(plan.type as any) !== SelfHostedPlan.enterprise && (
<UpgradeBtn
className='shrink-0'
isPlain={type === Plan.team}
isShort
loc={loc}
/>
)}
</div>
</div> </div>
{/* Plan detail */} {/* Plan detail */}
<div className='rounded-xl bg-white px-6 py-3'> <div className='p-2 grid content-start grid-cols-3 gap-1'>
<AppsInfo />
<UsageInfo <UsageInfo
className='py-3' Icon={RiGroupLine}
Icon={User01}
name={t('billing.usagePage.teamMembers')} name={t('billing.usagePage.teamMembers')}
usage={usage.teamMembers} usage={usage.teamMembers}
total={total.teamMembers} total={total.teamMembers}
/> />
<AppsInfo className='py-3' />
<VectorSpaceInfo className='py-3' />
<UsageInfo <UsageInfo
className='py-3' Icon={RiBook2Line}
Icon={MessageFastPlus}
name={t('billing.usagePage.annotationQuota')}
usage={usage.annotatedResponse}
total={total.annotatedResponse}
/>
<UsageInfo
className='py-3'
Icon={FileUpload}
name={t('billing.usagePage.documentsUploadQuota')} name={t('billing.usagePage.documentsUploadQuota')}
usage={usage.documentsUploadQuota} usage={usage.documentsUploadQuota}
total={total.documentsUploadQuota} total={total.documentsUploadQuota}
/> />
{isInHeader && type === Plan.sandbox && ( <VectorSpaceInfo />
<UpgradeBtn <UsageInfo
className='flex-shrink-0 my-3' Icon={RiFileEditLine}
isFull name={t('billing.usagePage.annotationQuota')}
size='lg' usage={usage.annotatedResponse}
isPlain={type !== Plan.sandbox} total={total.annotatedResponse}
loc={loc} />
/>
)}
</div> </div>
</div> </div>
) )

View File

@ -60,7 +60,6 @@ const Pricing: FC<Props> = ({
<div className='py-2 flex items-center justify-between h-[64px]'> <div className='py-2 flex items-center justify-between h-[64px]'>
<TabSlider <TabSlider
value={currentPlan} value={currentPlan}
itemWidth={170}
className='inline-flex' className='inline-flex'
options={[ options={[
{ {

View File

@ -1,18 +1,20 @@
import cn from '@/utils/classnames'
type ProgressBarProps = { type ProgressBarProps = {
percent: number percent: number
color: string color: string
} }
const ProgressBar = ({ const ProgressBar = ({
percent = 0, percent = 0,
color = '#2970FF', color = '#2970FF',
}: ProgressBarProps) => { }: ProgressBarProps) => {
return ( return (
<div className='bg-[#F2F4F7] rounded-[4px] overflow-hidden'> <div className='bg-components-progress-bar-bg rounded-[6px] overflow-hidden'>
<div <div
className='h-2 rounded-[4px]' className={cn('h-1 rounded-[6px]', color)}
style={{ style={{
width: `${Math.min(percent, 100)}%`, width: `${Math.min(percent, 100)}%`,
backgroundColor: color,
}} }}
/> />
</div> </div>

View File

@ -3,8 +3,8 @@ import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import PremiumBadge from '../../base/premium-badge' import PremiumBadge from '../../base/premium-badge'
import Button from '@/app/components/base/button'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common' import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import cn from '@/utils/classnames'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
type Props = { type Props = {
@ -17,23 +17,7 @@ type Props = {
loc?: string loc?: string
} }
const PlainBtn = ({ className, onClick }: { className?: string; onClick: () => void }) => {
const { t } = useTranslation()
return (
<div
className={cn(className, 'flex items-center h-8 px-3 rounded-lg border border-gray-200 bg-white shadow-sm cursor-pointer')}
onClick={onClick}
>
<div className='leading-[18px] text-[13px] font-medium text-gray-700'>
{t('billing.upgradeBtn.plain')}
</div>
</div>
)
}
const UpgradeBtn: FC<Props> = ({ const UpgradeBtn: FC<Props> = ({
className,
isPlain = false, isPlain = false,
isShort = false, isShort = false,
onClick: _onClick, onClick: _onClick,
@ -56,8 +40,13 @@ const UpgradeBtn: FC<Props> = ({
} }
} }
if (isPlain) if (isPlain) {
return <PlainBtn onClick={onClick} className={className} /> return (
<Button onClick={onClick}>
{t('billing.upgradeBtn.plain')}
</Button>
)
}
return ( return (
<PremiumBadge <PremiumBadge

View File

@ -2,7 +2,9 @@
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ChatBot } from '../../base/icons/src/vender/line/communication' import {
RiApps2Line,
} from '@remixicon/react'
import UsageInfo from '../usage-info' import UsageInfo from '../usage-info'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
@ -22,7 +24,7 @@ const AppsInfo: FC<Props> = ({
return ( return (
<UsageInfo <UsageInfo
className={className} className={className}
Icon={ChatBot} Icon={RiApps2Line}
name={t('billing.usagePage.buildApps')} name={t('billing.usagePage.buildApps')}
usage={usage.buildApps} usage={usage.buildApps}
total={total.buildApps} total={total.buildApps}

View File

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import ProgressBar from '../progress-bar' import ProgressBar from '../progress-bar'
import { NUM_INFINITE } from '../config' import { NUM_INFINITE } from '../config'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
type Props = { type Props = {
className?: string className?: string
@ -33,43 +34,37 @@ const UsageInfo: FC<Props> = ({
const percent = usage / total * 100 const percent = usage / total * 100
const color = (() => { const color = (() => {
if (percent < LOW) if (percent < LOW)
return '#155EEF' return 'bg-components-progress-bar-progress-solid'
if (percent < MIDDLE) if (percent < MIDDLE)
return '#F79009' return 'bg-components-progress-warning-progress'
return '#F04438' return 'bg-components-progress-error-progress'
})() })()
return ( return (
<div className={className}> <div className={cn('p-4 flex flex-col gap-2 rounded-xl bg-components-panel-bg', className)}>
<div className='flex justify-between h-5 items-center'> <Icon className='w-4 h-4 text-text-tertiary' />
<div className='flex items-center'> <div className='flex items-center gap-1'>
<Icon className='w-4 h-4 text-gray-700' /> <div className='system-xs-medium text-text-tertiary'>{name}</div>
<div className='mx-1 leading-5 text-sm font-medium text-gray-700'>{name}</div> {tooltip && (
{tooltip && ( <Tooltip
<Tooltip popupContent={
popupContent={ <div className='w-[180px]'>
<div className='w-[180px]'> {tooltip}
{tooltip} </div>
</div> }
} />
/> )}
)}
</div>
<div className='flex items-center leading-[18px] text-[13px] font-normal'>
<div style={{
color: percent < LOW ? '#344054' : color,
}}>{usage}{unit}</div>
<div className='mx-1 text-gray-300'>/</div>
<div className='text-gray-500'>{total === NUM_INFINITE ? t('billing.plansCommon.unlimited') : `${total}${unit}`}</div>
</div>
</div> </div>
<div className='mt-2'> <div className='flex items-center gap-1 system-md-semibold text-text-primary'>
<ProgressBar {usage}
percent={percent} <div className='system-md-regular text-text-quaternary'>/</div>
color={color} <div>{total === NUM_INFINITE ? t('billing.plansCommon.unlimited') : `${total}${unit}`}</div>
/>
</div> </div>
<ProgressBar
percent={percent}
color={color}
/>
</div> </div>
) )
} }

View File

@ -1,8 +1,10 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import {
RiHardDrive3Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ArtificialBrain } from '../../base/icons/src/vender/line/development'
import UsageInfo from '../usage-info' import UsageInfo from '../usage-info'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
@ -22,7 +24,7 @@ const VectorSpaceInfo: FC<Props> = ({
return ( return (
<UsageInfo <UsageInfo
className={className} className={className}
Icon={ArtificialBrain} Icon={RiHardDrive3Line}
name={t('billing.usagePage.vectorSpace')} name={t('billing.usagePage.vectorSpace')}
tooltip={t('billing.usagePage.vectorSpaceTooltip') as string} tooltip={t('billing.usagePage.vectorSpaceTooltip') as string}
usage={usage.vectorSpace} usage={usage.vectorSpace}

View File

@ -1,44 +1,36 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import CustomWebAppBrand from '../custom-web-app-brand' import CustomWebAppBrand from '../custom-web-app-brand'
import s from '../style.module.css'
import GridMask from '@/app/components/base/grid-mask'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type' import { Plan } from '@/app/components/billing/type'
import { contactSalesUrl } from '@/app/components/billing/config' import { contactSalesUrl } from '@/app/components/billing/config'
import { useModalContext } from '@/context/modal-context'
const CustomPage = () => { const CustomPage = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { plan, enableBilling } = useProviderContext() const { plan, enableBilling } = useProviderContext()
const { setShowPricingModal } = useModalContext()
const showBillingTip = enableBilling && plan.type === Plan.sandbox const showBillingTip = enableBilling && plan.type === Plan.sandbox
const showContact = enableBilling && (plan.type === Plan.professional || plan.type === Plan.team) const showContact = enableBilling && (plan.type === Plan.professional || plan.type === Plan.team)
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
{ {showBillingTip && (
showBillingTip && ( <div className='flex justify-between mb-1 p-4 pl-6 bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2 shadow-lg backdrop-blur-sm rounded-xl'>
<GridMask canvasClassName='!rounded-xl'> <div className='space-y-1 text-text-primary-on-surface'>
<div className='flex justify-between mb-1 px-6 py-5 h-[88px] shadow-md rounded-xl border-[0.5px] border-gray-200'> <div className='title-xl-semi-bold'>{t('custom.upgradeTip.title')}</div>
<div className={`${s.textGradient} leading-[24px] text-base font-semibold`}> <div className='system-sm-regular'>{t('custom.upgradeTip.des')}</div>
<div>{t('custom.upgradeTip.prefix')}</div>
<div>{t('custom.upgradeTip.suffix')}</div>
</div>
<UpgradeBtn />
</div>
</GridMask>
)
}
<CustomWebAppBrand />
{
showContact && (
<div className='absolute bottom-0 h-[50px] leading-[50px] text-xs text-gray-500'>
{t('custom.customize.prefix')}
<a className='text-[#155EEF]' href={contactSalesUrl} target='_blank' rel='noopener noreferrer'>{t('custom.customize.contactUs')}</a>
{t('custom.customize.suffix')}
</div> </div>
) <div className='w-[120px] h-10 flex items-center justify-center bg-white rounded-3xl shadow-xs system-md-semibold text-text-accent cursor-pointer hover:opacity-95' onClick={() => setShowPricingModal()}>{t('billing.upgradeBtn.encourageShort')}</div>
} </div>
)}
<CustomWebAppBrand />
{showContact && (
<div className='absolute bottom-0 h-[50px] leading-[50px] text-xs text-text-quaternary'>
{t('custom.customize.prefix')}
<a className='text-text-accent' href={contactSalesUrl} target='_blank' rel='noopener noreferrer'>{t('custom.customize.contactUs')}</a>
{t('custom.customize.suffix')}
</div>
)}
</div> </div>
) )
} }

View File

@ -2,22 +2,28 @@ import type { ChangeEvent } from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiEditBoxLine,
RiEqualizer2Line,
RiExchange2Fill,
RiImageAddLine,
RiLayoutLeft2Line,
RiLoader2Line, RiLoader2Line,
RiPlayLargeLine,
} from '@remixicon/react' } from '@remixicon/react'
import s from './style.module.css'
import LogoSite from '@/app/components/base/logo/logo-site' import LogoSite from '@/app/components/base/logo/logo-site'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication' import Divider from '@/app/components/base/divider'
import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type' import { Plan } from '@/app/components/billing/type'
import { imageUpload } from '@/app/components/base/image-uploader/utils' import { imageUpload } from '@/app/components/base/image-uploader/utils'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { BubbleTextMod } from '@/app/components/base/icons/src/vender/solid/communication'
import { import {
updateCurrentWorkspace, updateCurrentWorkspace,
} from '@/service/common' } from '@/service/common'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import cn from '@/utils/classnames'
const ALLOW_FILE_EXTENSIONS = ['svg', 'png'] const ALLOW_FILE_EXTENSIONS = ['svg', 'png']
@ -107,32 +113,7 @@ const CustomWebAppBrand = () => {
return ( return (
<div className='py-4'> <div className='py-4'>
<div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.webapp.title')}</div> <div className='flex items-center justify-between mb-2 p-4 rounded-xl bg-background-section-burn system-md-medium text-text-primary'>
<div className='relative mb-4 pl-4 pb-6 pr-[119px] rounded-xl border-[0.5px] border-black/8 shadow-xs bg-gray-50 overflow-hidden'>
<div className={`${s.mask} absolute top-0 left-0 w-full -bottom-2 z-10`}></div>
<div className='flex items-center -mt-2 mb-4 p-6 bg-white rounded-xl'>
<div className='flex items-center px-4 w-[125px] h-9 rounded-lg bg-primary-600 border-[0.5px] border-primary-700 shadow-xs'>
<MessageDotsCircle className='shrink-0 mr-2 w-4 h-4 text-white' />
<div className='grow h-2 rounded-sm bg-white opacity-50' />
</div>
</div>
<div className='flex items-center h-5 justify-between'>
<div className='w-[369px] h-1.5 rounded-sm bg-gray-200 opacity-80' />
{
!webappBrandRemoved && (
<div className='flex items-center text-[10px] font-medium text-gray-400'>
POWERED BY
{
webappLogo
? <img src={`${webappLogo}?hash=${imgKey}`} alt='logo' className='ml-2 block w-auto h-5' />
: <LogoSite className='ml-2 !h-5' />
}
</div>
)
}
</div>
</div>
<div className='flex items-center justify-between mb-2 px-4 h-14 rounded-xl border-[0.5px] border-gray-200 bg-gray-50 text-sm font-medium text-gray-900'>
{t('custom.webapp.removeBrand')} {t('custom.webapp.removeBrand')}
<Switch <Switch
size='l' size='l'
@ -141,34 +122,38 @@ const CustomWebAppBrand = () => {
onChange={handleSwitch} onChange={handleSwitch}
/> />
</div> </div>
<div className={` <div className={cn('flex items-center justify-between h-14 px-4 rounded-xl bg-background-section-burn', webappBrandRemoved && 'opacity-30')}>
flex items-center justify-between px-4 py-3 rounded-xl border-[0.5px] border-gray-200 bg-gray-50
${webappBrandRemoved && 'opacity-30'}
`}>
<div> <div>
<div className='leading-5 text-sm font-medium text-gray-900'>{t('custom.webapp.changeLogo')}</div> <div className='system-md-medium text-text-primary'>{t('custom.webapp.changeLogo')}</div>
<div className='leading-[18px] text-xs text-gray-500'>{t('custom.webapp.changeLogoTip')}</div> <div className='system-xs-regular text-text-tertiary'>{t('custom.webapp.changeLogoTip')}</div>
</div> </div>
<div className='flex items-center'> <div className='flex items-center'>
{(uploadDisabled || (!webappLogo && !webappBrandRemoved)) && (
<>
<Button
variant='ghost'
disabled={uploadDisabled || (!webappLogo && !webappBrandRemoved)}
onClick={handleRestore}
>
{t('custom.restore')}
</Button>
<div className='mx-2 h-5 w-[1px] bg-divider-regular'></div>
</>
)}
{ {
!uploading && ( !uploading && (
<Button <Button
className={` className='relative mr-2'
relative mr-2
`}
disabled={uploadDisabled} disabled={uploadDisabled}
> >
<ImagePlus className='mr-2 w-4 h-4' /> <RiImageAddLine className='mr-1 w-4 h-4' />
{ {
(webappLogo || fileId) (webappLogo || fileId)
? t('custom.change') ? t('custom.change')
: t('custom.upload') : t('custom.upload')
} }
<input <input
className={` className={cn('absolute block inset-0 opacity-0 text-[0] w-full', uploadDisabled ? 'cursor-not-allowed' : 'cursor-pointer')}
absolute block inset-0 opacity-0 text-[0] w-full
${uploadDisabled ? 'cursor-not-allowed' : 'cursor-pointer'}
`}
onClick={e => (e.target as HTMLInputElement).value = ''} onClick={e => (e.target as HTMLInputElement).value = ''}
type='file' type='file'
accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')} accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')}
@ -184,7 +169,7 @@ const CustomWebAppBrand = () => {
className='relative mr-2' className='relative mr-2'
disabled={true} disabled={true}
> >
<RiLoader2Line className='animate-spin mr-2 w-4 h-4' /> <RiLoader2Line className='animate-spin mr-1 w-4 h-4' />
{t('custom.uploading')} {t('custom.uploading')}
</Button> </Button>
) )
@ -192,6 +177,13 @@ const CustomWebAppBrand = () => {
{ {
fileId && ( fileId && (
<> <>
<Button
className='mr-2'
onClick={handleCancel}
disabled={webappBrandRemoved || !isCurrentWorkspaceManager}
>
{t('common.operation.cancel')}
</Button>
<Button <Button
variant='primary' variant='primary'
className='mr-2' className='mr-2'
@ -200,30 +192,126 @@ const CustomWebAppBrand = () => {
> >
{t('custom.apply')} {t('custom.apply')}
</Button> </Button>
<Button
className='mr-2'
onClick={handleCancel}
disabled={webappBrandRemoved || !isCurrentWorkspaceManager}
>
{t('common.operation.cancel')}
</Button>
</> </>
) )
} }
<div className='mr-2 h-5 w-[1px] bg-black/5'></div>
<Button
disabled={uploadDisabled || (!webappLogo && !webappBrandRemoved)}
onClick={handleRestore}
>
{t('custom.restore')}
</Button>
</div> </div>
</div> </div>
{ {uploadProgress === -1 && (
uploadProgress === -1 && ( <div className='mt-2 text-xs text-[#D92D20]'>{t('custom.uploadedFail')}</div>
<div className='mt-2 text-xs text-[#D92D20]'>{t('custom.uploadedFail')}</div> )}
) <div className='mt-5 mb-2 flex items-center gap-2'>
} <div className='shrink-0 system-xs-medium-uppercase text-text-tertiary'>{t('appOverview.overview.appInfo.preview')}</div>
<Divider bgStyle='gradient' className='grow' />
</div>
<div className='relative mb-2 flex items-center gap-3'>
{/* chat card */}
<div className='grow basis-1/2 h-[320px] flex bg-background-default-burn rounded-2xl border-[0.5px] border-components-panel-border-subtle overflow-hidden'>
<div className='shrink-0 h-full w-[232px] p-1 pr-0 flex flex-col'>
<div className='p-3 pr-2 flex items-center gap-3'>
<div className={cn('w-8 h-8 inline-flex items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-blue-light-solid')}>
<BubbleTextMod className='w-4 h-4 text-components-avatar-shape-fill-stop-100' />
</div>
<div className='grow system-md-semibold text-text-secondary'>Chatflow App</div>
<div className='p-1.5'>
<RiLayoutLeft2Line className='w-4 h-4 text-text-tertiary' />
</div>
</div>
<div className='shrink-0 px-4 py-3'>
<Button variant='secondary-accent' className='w-full justify-center'>
<RiEditBoxLine className='w-4 h-4 mr-1' />
<div className='p-1 opacity-20'>
<div className='h-2 w-[94px] rounded-sm bg-text-accent-light-mode-only'></div>
</div>
</Button>
</div>
<div className='grow px-3 pt-5'>
<div className='h-8 px-3 py-1 flex items-center'>
<div className='w-14 h-2 rounded-sm bg-text-quaternary opacity-20'></div>
</div>
<div className='h-8 px-3 py-1 flex items-center'>
<div className='w-[168px] h-2 rounded-sm bg-text-quaternary opacity-20'></div>
</div>
<div className='h-8 px-3 py-1 flex items-center'>
<div className='w-[128px] h-2 rounded-sm bg-text-quaternary opacity-20'></div>
</div>
</div>
<div className='shrink-0 p-3 flex items-center justify-between'>
<div className='p-1.5'>
<RiEqualizer2Line className='w-4 h-4 text-text-tertiary' />
</div>
<div className='flex items-center gap-1.5'>
{!webappBrandRemoved && (
<>
<div className='text-text-tertiary system-2xs-medium-uppercase'>POWERED BY</div>
{webappLogo
? <img src={`${webappLogo}?hash=${imgKey}`} alt='logo' className='block w-auto h-5' />
: <LogoSite className='!h-5' />
}
</>
)}
</div>
</div>
</div>
<div className='grow flex flex-col justify-between w-[138px] p-2 pr-0'>
<div className='grow pt-16 pl-[22px] pb-4 flex flex-col justify-between bg-chatbot-bg rounded-l-2xl border-[0.5px] border-r-0 border-components-panel-border-subtle'>
<div className='w-[720px] px-4 py-3 bg-chat-bubble-bg rounded-2xl border border-divider-subtle'>
<div className='mb-1 text-text-primary body-md-regular'>Hello! How can I assist you today?</div>
<Button size='small'>
<div className='w-[144px] h-2 rounded-sm bg-text-quaternary opacity-20'></div>
</Button>
</div>
<div className='w-[578px] h-[52px] flex items-center pl-3.5 rounded-xl bg-components-panel-bg-blur backdrop-blur-sm border border-components-chat-input-border shadow-md text-text-placeholder body-lg-regular'>Talk to Dify</div>
</div>
</div>
</div>
{/* workflow card */}
<div className='grow basis-1/2 h-[320px] flex flex-col bg-background-default-burn rounded-2xl border-[0.5px] border-components-panel-border-subtle overflow-hidden'>
<div className='w-full p-4 pb-0 border-b-[0.5px] border-divider-subtle'>
<div className='mb-2 flex items-center gap-3'>
<div className={cn('w-8 h-8 inline-flex items-center justify-center rounded-lg border border-divider-regular', 'bg-components-icon-bg-indigo-solid')}>
<RiExchange2Fill className='w-4 h-4 text-components-avatar-shape-fill-stop-100' />
</div>
<div className='grow system-md-semibold text-text-secondary'>Workflow App</div>
<div className='p-1.5'>
<RiLayoutLeft2Line className='w-4 h-4 text-text-tertiary' />
</div>
</div>
<div className='flex items-center gap-4'>
<div className='shrink-0 h-10 flex items-center border-b-2 border-components-tab-active text-text-primary system-md-semibold-uppercase'>RUN ONCE</div>
<div className='grow h-10 flex items-center border-b-2 border-transparent text-text-tertiary system-md-semibold-uppercase'>RUN BATCH</div>
</div>
</div>
<div className='grow bg-components-panel-bg'>
<div className='p-4 pb-1'>
<div className='mb-1 py-2'>
<div className='w-20 h-2 rounded-sm bg-text-quaternary opacity-20'></div>
</div>
<div className='w-full h-16 rounded-lg bg-components-input-bg-normal '></div>
</div>
<div className='px-4 py-3 flex items-center justify-between'>
<Button size='small'>
<div className='w-10 h-2 rounded-sm bg-text-quaternary opacity-20'></div>
</Button>
<Button variant='primary' size='small' disabled>
<RiPlayLargeLine className='mr-1 w-4 h-4' />
<span>Execute</span>
</Button>
</div>
</div>
<div className='shrink-0 h-12 p-4 pt-3 flex items-center gap-1.5 bg-components-panel-bg'>
{!webappBrandRemoved && (
<>
<div className='text-text-tertiary system-2xs-medium-uppercase'>POWERED BY</div>
{webappLogo
? <img src={`${webappLogo}?hash=${imgKey}`} alt='logo' className='block w-auto h-5' />
: <LogoSite className='!h-5' />
}
</>
)}
</div>
</div>
</div>
</div> </div>
) )
} }

View File

@ -31,7 +31,7 @@ const Field: FC<Props> = ({
return ( return (
<div className={cn(className)}> <div className={cn(className)}>
<div className='flex py-[7px]'> <div className='flex py-[7px]'>
<div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-gray-900')}>{label} </div> <div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-text-primary')}>{label} </div>
{isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>} {isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
{tooltip && ( {tooltip && (
<Tooltip <Tooltip

View File

@ -1,23 +1,25 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Webhooks } from '@/app/components/base/icons/src/vender/line/development' import {
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education' RiExternalLinkLine,
RiPuzzle2Line,
} from '@remixicon/react'
const Empty = () => { const Empty = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='mb-2 p-6 rounded-2xl bg-gray-50'> <div className='mb-2 p-6 rounded-xl bg-background-section'>
<div className='flex items-center justify-center mb-3 w-12 h-12 rounded-[10px] border border-[#EAECF5]'> <div className='flex items-center justify-center mb-3 w-10 h-10 rounded-[10px] bg-components-card-bg-alt backdrop-blur-sm border-[0.5px] border-components-card-border shadow-lg'>
<Webhooks className='w-6 h-6 text-gray-500' /> <RiPuzzle2Line className='w-5 h-5 text-text-accent' />
</div> </div>
<div className='mb-2 text-sm text-gray-600'>{t('common.apiBasedExtension.title')}</div> <div className='mb-1 text-text-secondary system-sm-medium'>{t('common.apiBasedExtension.title')}</div>
<a <a
className='flex items-center mb-2 h-[18px] text-xs text-primary-600' className='flex items-center system-xs-regular text-text-accent'
href={t('common.apiBasedExtension.linkUrl') || '/'} href={t('common.apiBasedExtension.linkUrl') || '/'}
target='_blank' rel='noopener noreferrer' target='_blank' rel='noopener noreferrer'
> >
<BookOpen01 className='mr-1 w-3 h-3' />
{t('common.apiBasedExtension.link')} {t('common.apiBasedExtension.link')}
<RiExternalLinkLine className='ml-1 w-3 h-3' />
</a> </a>
</div> </div>
) )

View File

@ -5,6 +5,7 @@ import {
} from '@remixicon/react' } from '@remixicon/react'
import Item from './item' import Item from './item'
import Empty from './empty' import Empty from './empty'
import Button from '@/app/components/base/button'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { fetchApiBasedExtensionList } from '@/service/common' import { fetchApiBasedExtensionList } from '@/service/common'
@ -41,13 +42,14 @@ const ApiBasedExtensionPage = () => {
)) ))
) )
} }
<div <Button
className='flex items-center justify-center px-3 h-8 text-[13px] font-medium text-gray-700 rounded-lg bg-gray-50 cursor-pointer' variant='secondary'
className='w-full'
onClick={handleOpenApiBasedExtensionModal} onClick={handleOpenApiBasedExtensionModal}
> >
<RiAddLine className='mr-2 w-4 h-4' /> <RiAddLine className='mr-1 w-4 h-4' />
{t('common.apiBasedExtension.add')} {t('common.apiBasedExtension.add')}
</div> </Button>
</div> </div>
) )
} }

View File

@ -3,8 +3,9 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
RiEditLine,
} from '@remixicon/react' } from '@remixicon/react'
import { Edit02 } from '@/app/components/base/icons/src/vender/line/general' import Button from '@/app/components/base/button'
import type { ApiBasedExtension } from '@/models/common' import type { ApiBasedExtension } from '@/models/common'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { deleteApiBasedExtension } from '@/service/common' import { deleteApiBasedExtension } from '@/service/common'
@ -36,25 +37,25 @@ const Item: FC<ItemProps> = ({
} }
return ( return (
<div className='group flex items-center mb-2 px-4 py-2 border-[0.5px] border-transparent rounded-xl bg-gray-50 hover:border-gray-200 hover:shadow-xs'> <div className='group flex items-center mb-2 px-4 py-2 border-[0.5px] border-transparent rounded-xl bg-components-input-bg-normal hover:border-components-input-border-active hover:shadow-xs'>
<div className='grow'> <div className='grow'>
<div className='mb-0.5 text-[13px] font-medium text-gray-700'>{data.name}</div> <div className='mb-0.5 text-[13px] font-medium text-text-secondary'>{data.name}</div>
<div className='text-xs text-gray-500'>{data.api_endpoint}</div> <div className='text-xs text-text-tertiary'>{data.api_endpoint}</div>
</div> </div>
<div className='hidden group-hover:flex items-center'> <div className='hidden group-hover:flex items-center'>
<div <Button
className='flex items-center mr-1 px-3 h-7 bg-white text-xs font-medium text-gray-700 rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer' className='mr-1'
onClick={handleOpenApiBasedExtensionModal} onClick={handleOpenApiBasedExtensionModal}
> >
<Edit02 className='mr-[5px] w-3.5 h-3.5' /> <RiEditLine className='mr-1 w-4 h-4' />
{t('common.operation.edit')} {t('common.operation.edit')}
</div> </Button>
<div <Button
className='flex items-center justify-center w-7 h-7 bg-white text-gray-700 rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer'
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
> >
<RiDeleteBinLine className='w-4 h-4' /> <RiDeleteBinLine className='mr-1 w-4 h-4' />
</div> {t('common.operation.delete')}
</Button>
</div> </div>
{ {
showDeleteConfirm showDeleteConfirm

View File

@ -101,9 +101,9 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({
<a <a
href={t('common.apiBasedExtension.linkUrl') || '/'} href={t('common.apiBasedExtension.linkUrl') || '/'}
target='_blank' rel='noopener noreferrer' target='_blank' rel='noopener noreferrer'
className='group flex items-center text-xs text-text-tertiary font-normal hover:text-text-accent' className='group flex items-center text-xs text-text-accent font-normal'
> >
<BookOpen01 className='mr-1 w-3 h-3 text-text-tertiary group-hover:text-text-accent' /> <BookOpen01 className='mr-1 w-3 h-3' />
{t('common.apiBasedExtension.link')} {t('common.apiBasedExtension.link')}
</a> </a>
</div> </div>

View File

@ -11,6 +11,7 @@ import {
import { Menu, Transition } from '@headlessui/react' import { Menu, Transition } from '@headlessui/react'
import { syncDataSourceNotion, updateDataSourceNotionAction } from '@/service/common' import { syncDataSourceNotion, updateDataSourceNotionAction } from '@/service/common'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import cn from '@/utils/classnames'
type OperateProps = { type OperateProps = {
payload: { payload: {
@ -23,13 +24,6 @@ export default function Operate({
payload, payload,
onAuthAgain, onAuthAgain,
}: OperateProps) { }: OperateProps) {
const itemClassName = `
flex px-3 py-2 hover:bg-gray-50 text-sm text-gray-700
cursor-pointer
`
const itemIconClassName = `
mr-2 mt-[2px] w-4 h-4 text-gray-500
`
const { t } = useTranslation() const { t } = useTranslation()
const { mutate } = useSWRConfig() const { mutate } = useSWRConfig()
@ -54,8 +48,8 @@ export default function Operate({
{ {
({ open }) => ( ({ open }) => (
<> <>
<Menu.Button className={`flex items-center justify-center w-8 h-8 rounded-lg hover:bg-gray-100 ${open && 'bg-gray-100'}`}> <Menu.Button className={cn('flex items-center justify-center w-8 h-8 rounded-lg hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
<RiMoreFill className='w-4 h-4' /> <RiMoreFill className='w-4 h-4 text-text-secondary' />
</Menu.Button> </Menu.Button>
<Transition <Transition
as={Fragment} as={Fragment}
@ -66,40 +60,34 @@ export default function Operate({
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items <Menu.Items className="absolute right-0 top-9 w-60 max-w-80 origin-top-right rounded-xl bg-components-panel-bg-blur backdrop-blur-sm border-[0.5px] border-components-panel-border shadow-lg">
className="
absolute right-0 top-9 w-60 max-w-80
divide-y divide-gray-100 origin-top-right rounded-lg bg-white
shadow-lg
"
>
<div className="px-1 py-1"> <div className="px-1 py-1">
<Menu.Item> <Menu.Item>
<div <div
className={itemClassName} className='flex px-3 py-2 hover:bg-state-base-hover rounded-lg cursor-pointer'
onClick={onAuthAgain} onClick={onAuthAgain}
> >
<RiStickyNoteAddLine className={itemIconClassName} /> <RiStickyNoteAddLine className='mr-2 mt-[2px] w-4 h-4 text-text-tertiary' />
<div> <div>
<div className='leading-5'>{t('common.dataSource.notion.changeAuthorizedPages')}</div> <div className='system-sm-semibold text-text-secondary'>{t('common.dataSource.notion.changeAuthorizedPages')}</div>
<div className='leading-5 text-xs text-gray-500'> <div className='text-text-tertiary system-xs-regular'>
{payload.total} {t('common.dataSource.notion.pagesAuthorized')} {payload.total} {t('common.dataSource.notion.pagesAuthorized')}
</div> </div>
</div> </div>
</div> </div>
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
<div className={itemClassName} onClick={handleSync}> <div className='flex px-3 py-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={handleSync}>
<RiLoopLeftLine className={itemIconClassName} /> <RiLoopLeftLine className='mr-2 mt-[2px] w-4 h-4 text-text-tertiary' />
<div className='leading-5'>{t('common.dataSource.notion.sync')}</div> <div className='system-sm-semibold text-text-secondary'>{t('common.dataSource.notion.sync')}</div>
</div> </div>
</Menu.Item> </Menu.Item>
</div> </div>
<Menu.Item> <Menu.Item>
<div className='p-1'> <div className='p-1 border-t border-divider-subtle'>
<div className={itemClassName} onClick={handleRemove}> <div className='flex px-3 py-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={handleRemove}>
<RiDeleteBinLine className={itemIconClassName} /> <RiDeleteBinLine className='mr-2 mt-[2px] w-4 h-4 text-text-tertiary' />
<div className='leading-5'>{t('common.dataSource.notion.remove')}</div> <div className='system-sm-semibold text-text-secondary'>{t('common.dataSource.notion.remove')}</div>
</div> </div>
</div> </div>
</Menu.Item> </Menu.Item>

View File

@ -89,11 +89,11 @@ const ConfigFirecrawlModal: FC<Props> = ({
return ( return (
<PortalToFollowElem open> <PortalToFollowElem open>
<PortalToFollowElemContent className='w-full h-full z-[60]'> <PortalToFollowElemContent className='w-full h-full z-[60]'>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'> <div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'> <div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-components-panel-bg shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'> <div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-4'> <div className='flex justify-between items-center mb-4'>
<div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.configFirecrawl`)}</div> <div className='system-xl-semibold text-text-primary'>{t(`${I18N_PREFIX}.configFirecrawl`)}</div>
</div> </div>
<div className='space-y-4'> <div className='space-y-4'>
@ -114,7 +114,7 @@ const ConfigFirecrawlModal: FC<Props> = ({
/> />
</div> </div>
<div className='my-8 flex justify-between items-center h-8'> <div className='my-8 flex justify-between items-center h-8'>
<a className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]' target='_blank' href='https://www.firecrawl.dev/account'> <a className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-text-accent' target='_blank' href='https://www.firecrawl.dev/account'>
<span>{t(`${I18N_PREFIX}.getApiKeyLinkText`)}</span> <span>{t(`${I18N_PREFIX}.getApiKeyLinkText`)}</span>
<LinkExternal02 className='w-3 h-3' /> <LinkExternal02 className='w-3 h-3' />
</a> </a>
@ -138,12 +138,12 @@ const ConfigFirecrawlModal: FC<Props> = ({
</div> </div>
</div> </div>
<div className='border-t-[0.5px] border-t-black/5'> <div className='border-t-[0.5px] border-t-divider-regular'>
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'> <div className='flex justify-center items-center py-3 bg-background-section-burn text-xs text-text-tertiary'>
<Lock01 className='mr-1 w-3 h-3 text-gray-500' /> <Lock01 className='mr-1 w-3 h-3 text-text-tertiary' />
{t('common.modelProvider.encrypted.front')} {t('common.modelProvider.encrypted.front')}
<a <a
className='text-primary-600 mx-1' className='text-text-accent mx-1'
target='_blank' rel='noopener noreferrer' target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html' href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
> >

View File

@ -75,11 +75,11 @@ const ConfigJinaReaderModal: FC<Props> = ({
return ( return (
<PortalToFollowElem open> <PortalToFollowElem open>
<PortalToFollowElemContent className='w-full h-full z-[60]'> <PortalToFollowElemContent className='w-full h-full z-[60]'>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'> <div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'> <div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-components-panel-bg shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'> <div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-4'> <div className='flex justify-between items-center mb-4'>
<div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.configJinaReader`)}</div> <div className='system-xl-semibold text-text-primary'>{t(`${I18N_PREFIX}.configJinaReader`)}</div>
</div> </div>
<div className='space-y-4'> <div className='space-y-4'>
@ -93,7 +93,7 @@ const ConfigJinaReaderModal: FC<Props> = ({
/> />
</div> </div>
<div className='my-8 flex justify-between items-center h-8'> <div className='my-8 flex justify-between items-center h-8'>
<a className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]' target='_blank' href='https://jina.ai/reader/'> <a className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-text-accent' target='_blank' href='https://jina.ai/reader/'>
<span>{t(`${I18N_PREFIX}.getApiKeyLinkText`)}</span> <span>{t(`${I18N_PREFIX}.getApiKeyLinkText`)}</span>
<LinkExternal02 className='w-3 h-3' /> <LinkExternal02 className='w-3 h-3' />
</a> </a>
@ -117,12 +117,12 @@ const ConfigJinaReaderModal: FC<Props> = ({
</div> </div>
</div> </div>
<div className='border-t-[0.5px] border-t-black/5'> <div className='border-t-[0.5px] border-t-divider-regular'>
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'> <div className='flex justify-center items-center py-3 bg-background-section-burn text-xs text-text-tertiary'>
<Lock01 className='mr-1 w-3 h-3 text-gray-500' /> <Lock01 className='mr-1 w-3 h-3 text-text-tertiary' />
{t('common.modelProvider.encrypted.front')} {t('common.modelProvider.encrypted.front')}
<a <a
className='text-primary-600 mx-1' className='text-text-accent mx-1'
target='_blank' rel='noopener noreferrer' target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html' href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
> >

View File

@ -85,10 +85,10 @@ const DataSourceWebsite: FC<Props> = ({ provider }) => {
logo: ({ className }: { className: string }) => ( logo: ({ className }: { className: string }) => (
item.provider === DataSourceProvider.fireCrawl item.provider === DataSourceProvider.fireCrawl
? ( ? (
<div className={cn(className, 'flex items-center justify-center w-5 h-5 bg-white border border-gray-100 text-xs font-medium text-gray-500 rounded ml-3')}>🔥</div> <div className={cn(className, 'flex items-center justify-center w-5 h-5 !bg-background-default border border-divider-subtle text-xs font-medium text-text-tertiary rounded ml-3')}>🔥</div>
) )
: ( : (
<div className={cn(className, 'flex items-center justify-center w-5 h-5 bg-white border border-gray-100 text-xs font-medium text-gray-500 rounded ml-3')}> <div className={cn(className, 'flex items-center justify-center w-5 h-5 !bg-background-default border border-divider-subtle text-xs font-medium text-text-tertiary rounded ml-3')}>
<span className={s.jinaLogo} /> <span className={s.jinaLogo} />
</div> </div>
) )

View File

@ -52,7 +52,7 @@ const ConfigItem: FC<Props> = ({
? <Indicator className='shrink-0 mr-[6px]' color='green' /> ? <Indicator className='shrink-0 mr-[6px]' color='green' />
: <Indicator className='shrink-0 mr-[6px]' color='yellow' /> : <Indicator className='shrink-0 mr-[6px]' color='yellow' />
} }
<div className={`shrink-0 mr-3 text-xs font-medium uppercase ${payload.isActive ? 'text-util-colors-green-green-600' : 'text-util-colors-warning-warning-600'}`}> <div className={`shrink-0 mr-3 system-xs-semibold-uppercase ${payload.isActive ? 'text-util-colors-green-green-600' : 'text-util-colors-warning-warning-600'}`}>
{ {
payload.isActive payload.isActive
? t(isNotion ? 'common.dataSource.notion.connected' : 'common.dataSource.website.active') ? t(isNotion ? 'common.dataSource.notion.connected' : 'common.dataSource.website.active')
@ -70,7 +70,7 @@ const ConfigItem: FC<Props> = ({
{ {
isWebsite && !readOnly && ( isWebsite && !readOnly && (
<div className='p-2 text-text-tertiary cursor-pointer rounded-md hover:bg-black/5' onClick={onRemove} > <div className='p-2 text-text-tertiary cursor-pointer rounded-md hover:bg-state-base-hover' onClick={onRemove} >
<RiDeleteBinLine className='w-4 h-4' /> <RiDeleteBinLine className='w-4 h-4' />
</div> </div>
) )

View File

@ -8,12 +8,13 @@ import ConfigItem from './config-item'
import s from './style.module.css' import s from './style.module.css'
import { DataSourceType } from './types' import { DataSourceType } from './types'
import Button from '@/app/components/base/button'
import { DataSourceProvider } from '@/models/common' import { DataSourceProvider } from '@/models/common'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
type: DataSourceType type: DataSourceType
provider: DataSourceProvider provider?: DataSourceProvider
isConfigured: boolean isConfigured: boolean
onConfigure: () => void onConfigure: () => void
readOnly: boolean readOnly: boolean
@ -43,13 +44,13 @@ const Panel: FC<Props> = ({
return ( return (
<div className='mb-2 bg-background-section-burn rounded-xl'> <div className='mb-2 bg-background-section-burn rounded-xl'>
<div className='flex items-center px-3 py-[9px]'> <div className='flex items-center px-3 py-[9px]'>
<div className={cn(s[`${type}-icon`], 'w-8 h-8 mr-3 border border-divider-subtle rounded-lg bg-background-default')} /> <div className={cn(s[`${type}-icon`], 'w-8 h-8 mr-3 border border-divider-subtle rounded-lg !bg-background-default')} />
<div className='grow'> <div className='grow'>
<div className='flex items-center h-5'> <div className='flex items-center h-5'>
<div className='text-sm font-medium text-text-primary'>{t(`common.dataSource.${type}.title`)}</div> <div className='text-sm font-medium text-text-primary'>{t(`common.dataSource.${type}.title`)}</div>
{isWebsite && ( {isWebsite && (
<div className='ml-1 leading-[18px] px-1.5 rounded-md bg-white border border-gray-100 text-xs font-medium text-gray-700'> <div className='ml-1 leading-[18px] px-1.5 rounded-md bg-components-badge-white-to-dark text-xs font-medium text-text-secondary'>
<span className='text-gray-500'>{t('common.dataSource.website.with')}</span> { provider === DataSourceProvider.fireCrawl ? '🔥 Firecrawl' : 'Jina Reader'} <span className='text-text-tertiary'>{t('common.dataSource.website.with')}</span> { provider === DataSourceProvider.fireCrawl ? '🔥 Firecrawl' : 'Jina Reader'}
</div> </div>
)} )}
</div> </div>
@ -66,16 +67,13 @@ const Panel: FC<Props> = ({
{ {
isConfigured isConfigured
? ( ? (
<div <Button
className={ disabled={readOnly}
`flex items-center ml-3 px-3 h-7 bg-white border border-gray-200 className='ml-3'
rounded-md text-xs font-medium text-gray-700
${!readOnly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
}
onClick={onConfigure} onClick={onConfigure}
> >
{t('common.dataSource.configure')} {t('common.dataSource.configure')}
</div> </Button>
) )
: ( : (
<> <>

View File

@ -194,12 +194,12 @@ export default function AccountSetting({
</div> </div>
<div ref={scrollRef} className='w-full pb-4 bg-components-panel-bg overflow-y-auto'> <div ref={scrollRef} className='w-full pb-4 bg-components-panel-bg overflow-y-auto'>
<div className={cn('sticky top-0 mx-8 pt-[27px] pb-2 mb-[18px] flex items-center bg-components-panel-bg z-20', scrolled && 'border-b border-divider-regular')}> <div className={cn('sticky top-0 mx-8 pt-[27px] pb-2 mb-[18px] flex items-center bg-components-panel-bg z-20', scrolled && 'border-b border-divider-regular')}>
<div className='shrink-0 text-text-primary title-2xl-semi-bold'>{activeItem?.name}</div> <div className='shrink-0 text-text-primary title-2xl-semi-bold'>
{ {activeItem?.name}
activeItem?.description && ( {activeItem?.description && (
<div className='shrink-0 ml-2 text-xs text-text-tertiary'>{activeItem?.description}</div> <div className='mt-1 system-sm-regular text-text-tertiary'>{activeItem?.description}</div>
) )}
} </div>
{activeItem?.key === 'provider' && ( {activeItem?.key === 'provider' && (
<div className='grow flex justify-end'> <div className='grow flex justify-end'>
<Input <Input

View File

@ -18,9 +18,11 @@ import type { InvitationResult } from '@/models/common'
import LogoEmbeddedChatHeader from '@/app/components/base/logo/logo-embedded-chat-header' import LogoEmbeddedChatHeader from '@/app/components/base/logo/logo-embedded-chat-header'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type' import { Plan } from '@/app/components/billing/type'
import Button from '@/app/components/base/button'
import UpgradeBtn from '@/app/components/billing/upgrade-btn' import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { NUM_INFINITE } from '@/app/components/billing/config' import { NUM_INFINITE } from '@/app/components/billing/config'
import { LanguagesSupported } from '@/i18n/language' import { LanguagesSupported } from '@/i18n/language'
import cn from '@/utils/classnames'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const MembersPage = () => { const MembersPage = () => {
@ -35,7 +37,13 @@ const MembersPage = () => {
const { locale } = useContext(I18n) const { locale } = useContext(I18n)
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext() const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext()
const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers) const { data, mutate } = useSWR(
{
url: '/workspaces/current/members',
params: {},
},
fetchMembers,
)
const [inviteModalVisible, setInviteModalVisible] = useState(false) const [inviteModalVisible, setInviteModalVisible] = useState(false)
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([]) const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
const [invitedModalVisible, setInvitedModalVisible] = useState(false) const [invitedModalVisible, setInvitedModalVisible] = useState(false)
@ -47,17 +55,17 @@ const MembersPage = () => {
return ( return (
<> <>
<div className='flex flex-col'> <div className='flex flex-col'>
<div className='flex items-center mb-4 p-3 gap-1 bg-gray-50 rounded-2xl'> <div className='flex items-center mb-4 p-3 pr-5 gap-3 bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 rounded-xl border-t-[0.5px] border-l-[0.5px] border-divider-subtle'>
<LogoEmbeddedChatHeader className='!w-10 !h-10' /> <LogoEmbeddedChatHeader className='!w-12 !h-12' />
<div className='grow mx-2'> <div className='grow'>
<div className='text-sm font-medium text-gray-900'>{currentWorkspace?.name}</div> <div className='system-md-semibold text-text-secondary'>{currentWorkspace?.name}</div>
{enableBilling && ( {enableBilling && (
<div className='text-xs text-gray-500'> <div className='mt-1 system-xs-medium text-text-tertiary'>
{isNotUnlimitedMemberPlan {isNotUnlimitedMemberPlan
? ( ? (
<div className='flex space-x-1'> <div className='flex space-x-1'>
<div>{t('billing.plansCommon.member')}{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}</div> <div>{t('billing.plansCommon.member')}{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}</div>
<div className='text-gray-700'>{accounts.length}</div> <div className=''>{accounts.length}</div>
<div>/</div> <div>/</div>
<div>{plan.total.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') : plan.total.teamMembers}</div> <div>{plan.total.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') : plan.total.teamMembers}</div>
</div> </div>
@ -75,14 +83,10 @@ const MembersPage = () => {
{isMemberFull && ( {isMemberFull && (
<UpgradeBtn className='mr-2' loc='member-invite' /> <UpgradeBtn className='mr-2' loc='member-invite' />
)} )}
<div className={ <Button variant='primary' className={cn('shrink-0')} disabled={!isCurrentWorkspaceManager || isMemberFull} onClick={() => setInviteModalVisible(true)}>
`shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200 <RiUserAddLine className='w-4 h-4 mr-1' />
text-[13px] font-medium text-primary-600 bg-white
shadow-xs rounded-lg ${(isCurrentWorkspaceManager && !isMemberFull) ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
} onClick={() => (isCurrentWorkspaceManager && !isMemberFull) && setInviteModalVisible(true)}>
<RiUserAddLine className='w-4 h-4 mr-2 ' />
{t('common.members.invite')} {t('common.members.invite')}
</div> </Button>
</div> </div>
<div className='overflow-visible lg:overflow-visible'> <div className='overflow-visible lg:overflow-visible'>
<div className='flex items-center py-[7px] border-b border-divider-regular min-w-[480px]'> <div className='flex items-center py-[7px] border-b border-divider-regular min-w-[480px]'>
@ -99,18 +103,18 @@ const MembersPage = () => {
<div className=''> <div className=''>
<div className='text-text-secondary system-sm-medium'> <div className='text-text-secondary system-sm-medium'>
{account.name} {account.name}
{account.status === 'pending' && <span className='ml-1 system-xs-regular text-[#DC6803]'>{t('common.members.pending')}</span>} {account.status === 'pending' && <span className='ml-1 system-xs-medium text-text-warning'>{t('common.members.pending')}</span>}
{userProfile.email === account.email && <span className='system-xs-regular text-text-tertiary'>{t('common.members.you')}</span>} {userProfile.email === account.email && <span className='system-xs-regular text-text-tertiary'>{t('common.members.you')}</span>}
</div> </div>
<div className='text-text-tertiary system-xs-regular'>{account.email}</div> <div className='text-text-tertiary system-xs-regular'>{account.email}</div>
</div> </div>
</div> </div>
<div className='shrink-0 flex items-center w-[104px] py-2 system-xs-regular text-text-secondary'>{dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}</div> <div className='shrink-0 flex items-center w-[104px] py-2 system-sm-regular text-text-secondary'>{dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}</div>
<div className='shrink-0 w-[96px] flex items-center'> <div className='shrink-0 w-[96px] flex items-center'>
{ {
((isCurrentWorkspaceOwner && account.role !== 'owner') || (isCurrentWorkspaceManager && !['owner', 'admin'].includes(account.role))) ((isCurrentWorkspaceOwner && account.role !== 'owner') || (isCurrentWorkspaceManager && !['owner', 'admin'].includes(account.role)))
? <Operation member={account} operatorRole={currentWorkspace.role} onOperate={mutate} /> ? <Operation member={account} operatorRole={currentWorkspace.role} onOperate={mutate} />
: <div className='px-3 system-xs-regular text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div> : <div className='px-3 system-sm-regular text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
} }
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { XMarkIcon } from '@heroicons/react/24/outline' import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ReactMultiEmail } from 'react-multi-email' import { ReactMultiEmail } from 'react-multi-email'
import { RiErrorWarningFill } from '@remixicon/react' import { RiErrorWarningFill } from '@remixicon/react'
@ -59,10 +59,10 @@ const InviteModal = ({
<div className={cn(s.wrap)}> <div className={cn(s.wrap)}>
<Modal overflowVisible isShow onClose={() => { }} className={cn(s.modal)}> <Modal overflowVisible isShow onClose={() => { }} className={cn(s.modal)}>
<div className='flex justify-between mb-2'> <div className='flex justify-between mb-2'>
<div className='text-xl font-semibold text-gray-900'>{t('common.members.inviteTeamMember')}</div> <div className='text-xl font-semibold text-text-primary'>{t('common.members.inviteTeamMember')}</div>
<XMarkIcon className='w-4 h-4 cursor-pointer' onClick={onCancel} /> <RiCloseLine className='w-4 h-4 cursor-pointer text-text-tertiary' onClick={onCancel} />
</div> </div>
<div className='mb-3 text-[13px] text-gray-500'>{t('common.members.inviteTeamMemberTip')}</div> <div className='mb-3 text-[13px] text-text-tertiary'>{t('common.members.inviteTeamMemberTip')}</div>
{!isEmailSetup && ( {!isEmailSetup && (
<div className='grow basis-0 overflow-y-auto pb-4'> <div className='grow basis-0 overflow-y-auto pb-4'>
<div className='relative mb-1 p-2 rounded-xl border border-components-panel-border shadow-xs'> <div className='relative mb-1 p-2 rounded-xl border border-components-panel-border shadow-xs'>
@ -80,19 +80,18 @@ const InviteModal = ({
)} )}
<div> <div>
<div className='mb-2 text-sm font-medium text-gray-900'>{t('common.members.email')}</div> <div className='mb-2 text-sm font-medium text-text-primary'>{t('common.members.email')}</div>
<div className='mb-8 h-36 flex items-stretch'> <div className='mb-8 h-36 flex items-stretch'>
<ReactMultiEmail <ReactMultiEmail
className={cn('w-full pt-2 px-3 outline-none border-none', className={cn('w-full pt-2 px-3 outline-none !bg-components-input-bg-normal border-components-input-border-active',
'appearance-none text-sm text-gray-900 rounded-lg overflow-y-auto', 'appearance-none text-sm !text-text-primary rounded-lg overflow-y-auto',
s.emailsInput,
)} )}
autoFocus autoFocus
emails={emails} emails={emails}
inputClassName='bg-transparent' inputClassName='bg-transparent'
onChange={setEmails} onChange={setEmails}
getLabel={(email, index, removeEmail) => getLabel={(email, index, removeEmail) =>
<div data-tag key={index} className={cn(s.emailBackground)}> <div data-tag key={index} className={cn('bg-components-button-secondary-bg')}>
<div data-tag-item>{email}</div> <div data-tag-item>{email}</div>
<span data-tag-handle onClick={() => removeEmail(index)}> <span data-tag-handle onClick={() => removeEmail(index)}>
× ×

View File

@ -34,53 +34,53 @@ const RoleSelector = ({ value, onChange }: RoleSelectorProps) => {
onClick={() => setOpen(v => !v)} onClick={() => setOpen(v => !v)}
className='block' className='block'
> >
<div className={cn('flex items-center px-3 py-2 rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}> <div className={cn('flex items-center px-3 py-2 rounded-lg bg-components-input-bg-normal cursor-pointer hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('common.members.invitedAsRole', { role: t(`common.members.${toHump(value)}`) })}</div> <div className='grow mr-2 text-text-primary text-sm leading-5'>{t('common.members.invitedAsRole', { role: t(`common.members.${toHump(value)}`) })}</div>
<RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' /> <RiArrowDownSLine className='shrink-0 w-4 h-4 text-text-secondary' />
</div> </div>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'> <PortalToFollowElemContent className='z-[1002]'>
<div className='relative w-[336px] bg-white rounded-lg border-[0.5px] bg-gray-200 shadow-lg'> <div className='relative w-[336px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg'>
<div className='p-1'> <div className='p-1'>
<div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => { <div className='p-2 rounded-lg hover:bg-state-base-hover cursor-pointer' onClick={() => {
onChange('normal') onChange('normal')
setOpen(false) setOpen(false)
}}> }}>
<div className='relative pl-5'> <div className='relative pl-5'>
<div className='text-gray-700 text-sm leading-5'>{t('common.members.normal')}</div> <div className='text-text-secondary text-sm leading-5'>{t('common.members.normal')}</div>
<div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.normalTip')}</div> <div className='text-text-tertiary text-xs leading-[18px]'>{t('common.members.normalTip')}</div>
{value === 'normal' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>} {value === 'normal' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-text-accent'/>}
</div> </div>
</div> </div>
<div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => { <div className='p-2 rounded-lg hover:bg-state-base-hover cursor-pointer' onClick={() => {
onChange('editor') onChange('editor')
setOpen(false) setOpen(false)
}}> }}>
<div className='relative pl-5'> <div className='relative pl-5'>
<div className='text-gray-700 text-sm leading-5'>{t('common.members.editor')}</div> <div className='text-text-secondary text-sm leading-5'>{t('common.members.editor')}</div>
<div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.editorTip')}</div> <div className='text-text-tertiary text-xs leading-[18px]'>{t('common.members.editorTip')}</div>
{value === 'editor' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>} {value === 'editor' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-text-accent'/>}
</div> </div>
</div> </div>
<div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => { <div className='p-2 rounded-lg hover:bg-state-base-hover cursor-pointer' onClick={() => {
onChange('admin') onChange('admin')
setOpen(false) setOpen(false)
}}> }}>
<div className='relative pl-5'> <div className='relative pl-5'>
<div className='text-gray-700 text-sm leading-5'>{t('common.members.admin')}</div> <div className='text-text-secondary text-sm leading-5'>{t('common.members.admin')}</div>
<div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.adminTip')}</div> <div className='text-text-tertiary text-xs leading-[18px]'>{t('common.members.adminTip')}</div>
{value === 'admin' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>} {value === 'admin' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-text-accent'/>}
</div> </div>
</div> </div>
{datasetOperatorEnabled && ( {datasetOperatorEnabled && (
<div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => { <div className='p-2 rounded-lg hover:bg-state-base-hover cursor-pointer' onClick={() => {
onChange('dataset_operator') onChange('dataset_operator')
setOpen(false) setOpen(false)
}}> }}>
<div className='relative pl-5'> <div className='relative pl-5'>
<div className='text-gray-700 text-sm leading-5'>{t('common.members.datasetOperator')}</div> <div className='text-text-secondary text-sm leading-5'>{t('common.members.datasetOperator')}</div>
<div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.datasetOperatorTip')}</div> <div className='text-text-tertiary text-xs leading-[18px]'>{t('common.members.datasetOperatorTip')}</div>
{value === 'dataset_operator' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>} {value === 'dataset_operator' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-text-accent'/>}
</div> </div>
</div> </div>
)} )}

View File

@ -1,7 +1,7 @@
.modal { .modal {
padding: 32px !important; padding: 32px !important;
width: 480px !important; width: 480px !important;
background: linear-gradient(180deg, rgba(3, 152, 85, 0.05) 0%, rgba(3, 152, 85, 0) 22.44%), #F9FAFB !important; /* background: linear-gradient(180deg, rgba(3, 152, 85, 0.05) 0%, rgba(3, 152, 85, 0) 22.44%), #F9FAFB !important; */
} }
.copyIcon { .copyIcon {

View File

@ -33,25 +33,25 @@ const InvitedModal = ({
<div className='flex justify-between mb-3'> <div className='flex justify-between mb-3'>
<div className=' <div className='
w-12 h-12 flex items-center justify-center rounded-xl w-12 h-12 flex items-center justify-center rounded-xl
bg-white border-[0.5px] border-gray-100 bg-background-section-burn border-[0.5px] border-components-panel-border
shadow-xl shadow-xl
'> '>
<CheckCircleIcon className='w-[22px] h-[22px] text-[#039855]' /> <CheckCircleIcon className='w-[22px] h-[22px] text-[#039855]' />
</div> </div>
<XMarkIcon className='w-4 h-4 cursor-pointer' onClick={onCancel} /> <XMarkIcon className='w-4 h-4 cursor-pointer' onClick={onCancel} />
</div> </div>
<div className='mb-1 text-xl font-semibold text-gray-900'>{t('common.members.invitationSent')}</div> <div className='mb-1 text-xl font-semibold text-text-primary'>{t('common.members.invitationSent')}</div>
{!IS_CE_EDITION && ( {!IS_CE_EDITION && (
<div className='mb-10 text-sm text-gray-500'>{t('common.members.invitationSentTip')}</div> <div className='mb-10 text-sm text-text-tertiary'>{t('common.members.invitationSentTip')}</div>
)} )}
{IS_CE_EDITION && ( {IS_CE_EDITION && (
<> <>
<div className='mb-5 text-sm text-gray-500'>{t('common.members.invitationSentTip')}</div> <div className='mb-5 text-sm text-text-tertiary'>{t('common.members.invitationSentTip')}</div>
<div className='flex flex-col gap-2 mb-9'> <div className='flex flex-col gap-2 mb-9'>
{ {
!!successInvitationResults.length !!successInvitationResults.length
&& <> && <>
<div className='py-2 text-sm font-Medium text-gray-900'>{t('common.members.invitationLink')}</div> <div className='py-2 text-sm font-Medium text-text-primary'>{t('common.members.invitationLink')}</div>
{successInvitationResults.map(item => {successInvitationResults.map(item =>
<InvitationLink key={item.email} value={item} />)} <InvitationLink key={item.email} value={item} />)}
</> </>
@ -59,7 +59,7 @@ const InvitedModal = ({
{ {
!!failedInvitationResults.length !!failedInvitationResults.length
&& <> && <>
<div className='py-2 text-sm font-Medium text-gray-900'>{t('common.members.failedInvitationEmails')}</div> <div className='py-2 text-sm font-Medium text-text-primary'>{t('common.members.failedInvitationEmails')}</div>
<div className='flex flex-wrap justify-between gap-y-1'> <div className='flex flex-wrap justify-between gap-y-1'>
{ {
failedInvitationResults.map(item => failedInvitationResults.map(item =>

View File

@ -35,21 +35,21 @@ const InvitationLink = ({
}, [isCopied]) }, [isCopied])
return ( return (
<div className='flex rounded-lg bg-gray-100 hover:bg-gray-100 border border-gray-200 py-2 items-center'> <div className='flex rounded-lg bg-components-input-bg-normal hover:bg-state-base-hover border border-components-input-border-active py-2 items-center'>
<div className="flex items-center flex-grow h-5"> <div className="flex items-center grow h-5">
<div className='flex-grow bg-gray-100 text-[13px] relative h-full'> <div className='grow text-[13px] relative h-full'>
<Tooltip <Tooltip
popupContent={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`} popupContent={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
> >
<div className='absolute top-0 left-0 w-full pl-2 pr-2 truncate cursor-pointer r-0' onClick={copyHandle}>{value.url}</div> <div className='absolute top-0 left-0 w-full pl-2 pr-2 truncate cursor-pointer r-0' onClick={copyHandle}>{value.url}</div>
</Tooltip> </Tooltip>
</div> </div>
<div className="flex-shrink-0 h-4 bg-gray-200 border" /> <div className="shrink-0 h-4 bg-divider-regular border" />
<Tooltip <Tooltip
popupContent={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`} popupContent={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
> >
<div className="px-0.5 flex-shrink-0"> <div className="px-0.5 shrink-0">
<div className={`box-border w-[30px] h-[30px] flex items-center justify-center rounded-lg hover:bg-gray-100 cursor-pointer ${s.copyIcon} ${isCopied ? s.copied : ''}`} onClick={copyHandle}> <div className={`box-border w-[30px] h-[30px] flex items-center justify-center rounded-lg hover:bg-state-base-hover cursor-pointer ${s.copyIcon} ${isCopied ? s.copied : ''}`} onClick={copyHandle}>
</div> </div>
</div> </div>
</Tooltip> </Tooltip>

View File

@ -1,3 +0,0 @@
.popup {
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
}

View File

@ -4,26 +4,12 @@ import { Fragment, useMemo } from 'react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { Menu, Transition } from '@headlessui/react' import { Menu, Transition } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/outline' import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/outline'
import s from './index.module.css'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { Member } from '@/models/common' import type { Member } from '@/models/common'
import { deleteMemberOrCancelInvitation, updateMemberRole } from '@/service/common' import { deleteMemberOrCancelInvitation, updateMemberRole } from '@/service/common'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
const itemClassName = `
flex px-3 py-2 cursor-pointer hover:bg-gray-50 rounded-lg
`
const itemIconClassName = `
w-4 h-4 mt-[2px] mr-1 text-primary-600
`
const itemTitleClassName = `
leading-[20px] text-sm text-gray-700 whitespace-nowrap
`
const itemDescClassName = `
leading-[18px] text-xs text-gray-500 whitespace-nowrap
`
type IOperationProps = { type IOperationProps = {
member: Member member: Member
operatorRole: string operatorRole: string
@ -90,15 +76,9 @@ const Operation = ({
{ {
({ open }) => ( ({ open }) => (
<> <>
<Menu.Button className={cn( <Menu.Button className={cn('group px-3 flex items-center justify-between w-full h-full system-sm-regular text-text-secondary cursor-pointer hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
`
group flex items-center justify-between w-full h-full
hover:bg-gray-100 cursor-pointer ${open && 'bg-gray-100'}
text-[13px] text-gray-700 px-3
`,
)}>
{RoleMap[member.role] || RoleMap.normal} {RoleMap[member.role] || RoleMap.normal}
<ChevronDownIcon className={`w-4 h-4 group-hover:block ${open ? 'block' : 'hidden'}`} /> <ChevronDownIcon className={cn('w-4 h-4 group-hover:block', open ? 'block' : 'hidden')} />
</Menu.Button> </Menu.Button>
<Transition <Transition
as={Fragment} as={Fragment}
@ -110,27 +90,21 @@ const Operation = ({
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items <Menu.Items
className={cn( className={cn('absolute origin-top-right right-0 top-[52px] z-10 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg')}
`
absolute right-0 top-[52px] z-10 bg-white border-[0.5px] border-gray-200
divide-y divide-gray-100 origin-top-right rounded-lg
`,
s.popup,
)}
> >
<div className="px-1 py-1"> <div className="p-1">
{ {
roleList.map(role => ( roleList.map(role => (
<Menu.Item key={role}> <Menu.Item key={role}>
<div className={itemClassName} onClick={() => handleUpdateMemberRole(role)}> <div className='flex px-3 py-2 cursor-pointer hover:bg-state-base-hover rounded-lg' onClick={() => handleUpdateMemberRole(role)}>
{ {
role === member.role role === member.role
? <CheckIcon className={itemIconClassName} /> ? <CheckIcon className='w-4 h-4 mt-[2px] mr-1 text-text-accent' />
: <div className={itemIconClassName} /> : <div className='w-4 h-4 mt-[2px] mr-1 text-text-accent' />
} }
<div> <div>
<div className={itemTitleClassName}>{t(`common.members.${toHump(role)}`)}</div> <div className='system-sm-semibold text-text-secondary whitespace-nowrap'>{t(`common.members.${toHump(role)}`)}</div>
<div className={itemDescClassName}>{t(`common.members.${toHump(role)}Tip`)}</div> <div className='system-xs-regular text-text-tertiary whitespace-nowrap'>{t(`common.members.${toHump(role)}Tip`)}</div>
</div> </div>
</div> </div>
</Menu.Item> </Menu.Item>
@ -138,12 +112,12 @@ const Operation = ({
} }
</div> </div>
<Menu.Item> <Menu.Item>
<div className='px-1 py-1'> <div className='p-1 border-t border-divider-subtle'>
<div className={itemClassName} onClick={handleDeleteMemberOrCancelInvitation}> <div className='flex px-3 py-2 cursor-pointer hover:bg-state-base-hover rounded-lg' onClick={handleDeleteMemberOrCancelInvitation}>
<div className={itemIconClassName} /> <div className='w-4 h-4 mt-[2px] mr-1 text-text-accent' />
<div> <div>
<div className={itemTitleClassName}>{t('common.members.removeFromTeam')}</div> <div className='system-sm-semibold text-text-secondary whitespace-nowrap'>{t('common.members.removeFromTeam')}</div>
<div className={itemDescClassName}>{t('common.members.removeFromTeamTip')}</div> <div className='system-xs-regular text-text-tertiary whitespace-nowrap'>{t('common.members.removeFromTeamTip')}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -65,7 +65,7 @@ const ListWithCollection = ({
return ( return (
<CardWrapper <CardWrapper
key={plugin.name} key={plugin.plugin_id}
plugin={plugin} plugin={plugin}
showInstallButton={showInstallButton} showInstallButton={showInstallButton}
locale={locale} locale={locale}

View File

@ -425,7 +425,10 @@
.markdown-body ol { .markdown-body ol {
padding-left: 2em; padding-left: 2em;
} }
.markdown-body ul[role="listbox"] {
list-style: none !important;
padding-left: 0 !important;
}
.markdown-body blockquote> :first-child { .markdown-body blockquote> :first-child {
margin-top: 0; margin-top: 0;
} }

View File

@ -155,7 +155,7 @@ const translation = {
}, },
langfuse: { langfuse: {
title: 'Langfuse', title: 'Langfuse',
description: 'Traces, evals, prompt management and metrics to debug and improve your LLM application.', description: 'Open-source LLM observability, evaluation, prompt management and metrics to debug and improve your LLM application.',
}, },
opik: { opik: {
title: 'Opik', title: 'Opik',

View File

@ -10,7 +10,7 @@ const translation = {
}, },
teamMembers: 'Team Members', teamMembers: 'Team Members',
upgradeBtn: { upgradeBtn: {
plain: 'Upgrade Plan', plain: 'View Plan',
encourage: 'Upgrade Now', encourage: 'Upgrade Now',
encourageShort: 'Upgrade', encourageShort: 'Upgrade',
}, },
@ -101,18 +101,22 @@ const translation = {
plans: { plans: {
sandbox: { sandbox: {
name: 'Sandbox', name: 'Sandbox',
for: 'Free Trial of Core Capabilities',
description: 'Free Trial of Core Capabilities', description: 'Free Trial of Core Capabilities',
}, },
professional: { professional: {
name: 'Professional', name: 'Professional',
for: 'For Independent Developers/Small Teams',
description: 'For Independent Developers/Small Teams', description: 'For Independent Developers/Small Teams',
}, },
team: { team: {
name: 'Team', name: 'Team',
for: 'For Medium-sized Teams',
description: 'For Medium-sized Teams', description: 'For Medium-sized Teams',
}, },
community: { community: {
name: 'Community', name: 'Community',
for: 'For Individual Users, Small Teams, or Non-commercial Projects',
description: 'For Individual Users, Small Teams, or Non-commercial Projects', description: 'For Individual Users, Small Teams, or Non-commercial Projects',
price: 'Free', price: 'Free',
btnText: 'Get Started with Community', btnText: 'Get Started with Community',
@ -125,6 +129,7 @@ const translation = {
}, },
premium: { premium: {
name: 'Premium', name: 'Premium',
for: 'For Mid-sized Organizations and Teams',
description: 'For Mid-sized Organizations and Teams', description: 'For Mid-sized Organizations and Teams',
price: 'Scalable', price: 'Scalable',
priceTip: 'Based on Cloud Marketplace', priceTip: 'Based on Cloud Marketplace',
@ -140,6 +145,7 @@ const translation = {
}, },
enterprise: { enterprise: {
name: 'Enterprise', name: 'Enterprise',
for: 'For large-sized Teams',
description: 'For Enterprise Require Organization-wide Security, Compliance, Scalability, Control and More Advanced Features', description: 'For Enterprise Require Organization-wide Security, Compliance, Scalability, Control and More Advanced Features',
price: 'Custom', price: 'Custom',
priceTip: 'Annual Billing Only', priceTip: 'Annual Billing Only',

View File

@ -1,6 +1,8 @@
const translation = { const translation = {
custom: 'Customization', custom: 'Customization',
upgradeTip: { upgradeTip: {
title: 'Upgrade your plan',
des: 'Upgrade your plan to customize your brand',
prefix: 'Upgrade your plan to', prefix: 'Upgrade your plan to',
suffix: 'customize your brand.', suffix: 'customize your brand.',
}, },

View File

@ -37,11 +37,11 @@ const translation = {
}, },
operation: { operation: {
detail: '세부 정보', detail: '세부 정보',
install: '설치하다', install: '설치',
viewDetail: '자세히보기', viewDetail: '자세히보기',
info: '플러그인 정보', info: '플러그인 정보',
update: '업데이트', update: '업데이트',
remove: '제거하다', remove: '제거',
checkUpdate: '업데이트 확인', checkUpdate: '업데이트 확인',
}, },
toolSelector: { toolSelector: {
@ -178,7 +178,7 @@ const translation = {
pluginsResult: '{{num}} 결과', pluginsResult: '{{num}} 결과',
discover: '발견하다', discover: '발견하다',
moreFrom: 'Marketplace에서 더 보기', moreFrom: 'Marketplace에서 더 보기',
sortBy: '블랙 시티', sortBy: '정렬',
and: '그리고', and: '그리고',
}, },
task: { task: {

View File

@ -9,7 +9,7 @@ const translation = {
vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。', vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。',
}, },
upgradeBtn: { upgradeBtn: {
plain: '升级套餐', plain: '查看套餐',
encourage: '立即升级', encourage: '立即升级',
encourageShort: '升级', encourageShort: '升级',
}, },
@ -100,18 +100,22 @@ const translation = {
plans: { plans: {
sandbox: { sandbox: {
name: 'Sandbox', name: 'Sandbox',
for: '核心能力的免费试用',
description: '核心功能免费试用', description: '核心功能免费试用',
}, },
professional: { professional: {
name: 'Professional', name: 'Professional',
for: '适合独立开发者或小团队',
description: '对于独立开发者/小团队', description: '对于独立开发者/小团队',
}, },
team: { team: {
name: 'Team', name: 'Team',
for: '适合中等规模的团队',
description: '对于中型团队', description: '对于中型团队',
}, },
community: { community: {
name: 'Community', name: 'Community',
for: '适用于个人用户、小型团队或非商业项目',
description: '适用于个人用户、小型团队或非商业项目', description: '适用于个人用户、小型团队或非商业项目',
price: '免费', price: '免费',
btnText: '开始使用', btnText: '开始使用',
@ -124,6 +128,7 @@ const translation = {
}, },
premium: { premium: {
name: 'Premium', name: 'Premium',
for: '对于中型组织和团队',
description: '对于中型组织和团队', description: '对于中型组织和团队',
price: '可扩展', price: '可扩展',
priceTip: '基于云市场', priceTip: '基于云市场',
@ -139,6 +144,7 @@ const translation = {
}, },
enterprise: { enterprise: {
name: 'Enterprise', name: 'Enterprise',
for: '适合大人员规模的团队',
description: '对于需要组织范围内的安全性、合规性、可扩展性、控制和更高级功能的企业', description: '对于需要组织范围内的安全性、合规性、可扩展性、控制和更高级功能的企业',
price: '定制', price: '定制',
priceTip: '仅按年计费', priceTip: '仅按年计费',

View File

@ -1,6 +1,8 @@
const translation = { const translation = {
custom: '定制', custom: '定制',
upgradeTip: { upgradeTip: {
title: '升级您的计划',
des: '升级您的计划来定制您的品牌。',
prefix: '升级您的计划以', prefix: '升级您的计划以',
suffix: '定制您的品牌。', suffix: '定制您的品牌。',
}, },