mirror of https://github.com/langgenius/dify.git
Plugin/merge main 20250208 (#13414)
Signed-off-by: yihong0618 <zouzou0208@gmail.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: xhe <xw897002528@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: kurokobo <kuro664@gmail.com> Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com> Co-authored-by: NFish <douxc512@gmail.com> Co-authored-by: Gen Sato <52241300+halogen22@users.noreply.github.com> Co-authored-by: eux <euxuuu@gmail.com> Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com> Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com> Co-authored-by: lotsik <lotsik@mail.ru> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: gakkiyomi <gakkiyomi@aliyun.com> Co-authored-by: CN-P5 <heibai2006@gmail.com> Co-authored-by: CN-P5 <heibai2006@qq.com> Co-authored-by: Chuehnone <1897025+chuehnone@users.noreply.github.com> Co-authored-by: yihong <zouzou0208@gmail.com> Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Boris Feld <lothiraldan@gmail.com> Co-authored-by: mbo <himabo@gmail.com> Co-authored-by: mabo <mabo@aeyes.ai> Co-authored-by: Warren Chen <warren.chen830@gmail.com> Co-authored-by: KVOJJJin <jzongcode@gmail.com> Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com> Co-authored-by: jiandanfeng <chenjh3@wangsu.com> Co-authored-by: zhu-an <70234959+xhdd123321@users.noreply.github.com> Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com> Co-authored-by: 海狸大師 <86974027+yenslife@users.noreply.github.com> Co-authored-by: Xu Song <xusong.vip@gmail.com> Co-authored-by: rayshaw001 <396301947@163.com> Co-authored-by: Ding Jiatong <dingjiatong@gmail.com> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: JasonVV <jasonwangiii@outlook.com> Co-authored-by: le0zh <newlight@qq.com> Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com> Co-authored-by: k-zaku <zaku99@outlook.jp> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: luckylhb90 <luckylhb90@gmail.com> Co-authored-by: hobo.l <hobo.l@binance.com> Co-authored-by: jiangbo721 <365065261@qq.com> Co-authored-by: 刘江波 <jiangbo721@163.com> Co-authored-by: Shun Miyazawa <34241526+miya@users.noreply.github.com> Co-authored-by: EricPan <30651140+Egfly@users.noreply.github.com> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Jhvcc <37662342+Jhvcc@users.noreply.github.com> Co-authored-by: lowell <lowell.hu@zkteco.in> Co-authored-by: Ademílson Tonato <ademilsonft@outlook.com> Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com> Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com> Co-authored-by: Yueh-Po Peng (Yabi) <94939112+y10ab1@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Jason <ggbbddjm@gmail.com> Co-authored-by: Xin Zhang <sjhpzx@gmail.com> Co-authored-by: yjc980121 <3898524+yjc980121@users.noreply.github.com> Co-authored-by: heyszt <36215648+hieheihei@users.noreply.github.com> Co-authored-by: Abdullah AlOsaimi <osaimiacc@gmail.com> Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com> Co-authored-by: Yingchun Lai <laiyingchun@apache.org> Co-authored-by: Hash Brown <hi@xzd.me> Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com> Co-authored-by: Masashi Tomooka <tmokmss@users.noreply.github.com> Co-authored-by: aplio <ryo.091219@gmail.com> Co-authored-by: Obada Khalili <54270856+obadakhalili@users.noreply.github.com> Co-authored-by: Nam Vu <zuzoovn@gmail.com> Co-authored-by: Kei YAMAZAKI <1715090+kei-yamazaki@users.noreply.github.com> Co-authored-by: TechnoHouse <13776377+deephbz@users.noreply.github.com> Co-authored-by: Riddhimaan-Senapati <114703025+Riddhimaan-Senapati@users.noreply.github.com> Co-authored-by: MaFee921 <31881301+2284730142@users.noreply.github.com> Co-authored-by: te-chan <t-nakanome@sakura-is.co.jp> Co-authored-by: HQidea <HQidea@users.noreply.github.com> Co-authored-by: Joshbly <36315710+Joshbly@users.noreply.github.com> Co-authored-by: xhe <xw897002528@gmail.com> Co-authored-by: weiwenyan-dev <154779315+weiwenyan-dev@users.noreply.github.com> Co-authored-by: ex_wenyan.wei <ex_wenyan.wei@tcl.com> Co-authored-by: engchina <12236799+engchina@users.noreply.github.com> Co-authored-by: engchina <atjapan2015@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: Kemal <kemalmeler@outlook.com> Co-authored-by: Lazy_Frog <4590648+lazyFrogLOL@users.noreply.github.com> Co-authored-by: Novice Lee <novicelee@NoviPro.local> Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com> Co-authored-by: Steven sun <98230804+Tuyohai@users.noreply.github.com> Co-authored-by: steven <sunzwj@digitalchina.com> Co-authored-by: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com> Co-authored-by: Katy Tao <34019945+KatyTao@users.noreply.github.com> Co-authored-by: depy <42985524+h4ckdepy@users.noreply.github.com> Co-authored-by: 胡春东 <gycm520@gmail.com> Co-authored-by: Junjie.M <118170653@qq.com>
This commit is contained in:
parent
4d25b598f9
commit
4a43e165fb
|
|
@ -0,0 +1,47 @@
|
|||
name: Build docker image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- api/Dockerfile
|
||||
- web/Dockerfile
|
||||
|
||||
concurrency:
|
||||
group: docker-build-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- service_name: "api-amd64"
|
||||
platform: linux/amd64
|
||||
context: "api"
|
||||
- service_name: "api-arm64"
|
||||
platform: linux/arm64
|
||||
context: "api"
|
||||
- service_name: "web-amd64"
|
||||
platform: linux/amd64
|
||||
context: "web"
|
||||
- service_name: "web-arm64"
|
||||
platform: linux/arm64
|
||||
context: "web"
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: false
|
||||
context: "{{defaultContext}}:${{ matrix.context }}"
|
||||
platforms: ${{ matrix.platform }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
@ -25,6 +25,9 @@
|
|||
<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">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<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">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<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">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<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="seguir en 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="seguir en LinkedIn"></a>
|
||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
||||
<img alt="Descargas de Docker" 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">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<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="suivre sur 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="suivre sur LinkedIn"></a>
|
||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
||||
<img alt="Tirages Docker" 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">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<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="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="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">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<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">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<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">
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@
|
|||
<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">
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@
|
|||
<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">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<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="X(Twitter)'da takip et"></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="LinkedIn'da takip et"></a>
|
||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
||||
<img alt="Docker Çekmeleri" 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">
|
||||
|
|
@ -62,8 +65,6 @@ Görsel bir arayüz üzerinde güçlü AI iş akışları oluşturun ve test edi
|
|||

|
||||
|
||||
|
||||
Özür dilerim, haklısınız. Daha anlamlı ve akıcı bir çeviri yapmaya çalışayım. İşte güncellenmiş çeviri:
|
||||
|
||||
**3. Prompt IDE**:
|
||||
Komut istemlerini oluşturmak, model performansını karşılaştırmak ve sohbet tabanlı uygulamalara metin-konuşma gibi ek özellikler eklemek için kullanıcı dostu bir arayüz.
|
||||
|
||||
|
|
@ -150,8 +151,6 @@ Görsel bir arayüz üzerinde güçlü AI iş akışları oluşturun ve test edi
|
|||
## Dify'ı Kullanma
|
||||
|
||||
- **Cloud </br>**
|
||||
İşte verdiğiniz metnin Türkçe çevirisi, kod bloğu içinde:
|
||||
-
|
||||
Herkesin sıfır kurulumla denemesi için bir [Dify Cloud](https://dify.ai) hizmeti sunuyoruz. Bu hizmet, kendi kendine dağıtılan versiyonun tüm yeteneklerini sağlar ve sandbox planında 200 ücretsiz GPT-4 çağrısı içerir.
|
||||
|
||||
- **Dify Topluluk Sürümünü Kendi Sunucunuzda Barındırma</br>**
|
||||
|
|
@ -177,8 +176,6 @@ GitHub'da Dify'a yıldız verin ve yeni sürümlerden anında haberdar olun.
|
|||
>- RAM >= 4GB
|
||||
|
||||
</br>
|
||||
İşte verdiğiniz metnin Türkçe çevirisi, kod bloğu içinde:
|
||||
|
||||
Dify sunucusunu başlatmanın en kolay yolu, [docker-compose.yml](docker/docker-compose.yaml) dosyamızı çalıştırmaktır. Kurulum komutunu çalıştırmadan önce, makinenizde [Docker](https://docs.docker.com/get-docker/) ve [Docker Compose](https://docs.docker.com/compose/install/)'un kurulu olduğundan emin olun:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<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="theo dõi trên 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="theo dõi trên 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">
|
||||
|
|
|
|||
|
|
@ -48,16 +48,18 @@ ENV TZ=UTC
|
|||
|
||||
WORKDIR /app/api
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl nodejs libgmp-dev libmpfr-dev libmpc-dev \
|
||||
# if you located in China, you can use aliyun mirror to speed up
|
||||
# && echo "deb http://mirrors.aliyun.com/debian testing main" > /etc/apt/sources.list \
|
||||
&& echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
# For Security
|
||||
# && apt-get install -y --no-install-recommends expat=2.6.4-1 libldap-2.5-0=2.5.19+dfsg-1 perl=5.40.0-8 libsqlite3-0=3.46.1-1 zlib1g=1:1.3.dfsg+really1.3.1-1+b1 \
|
||||
# install a chinese font to support the use of tools like matplotlib
|
||||
&& apt-get install -y fonts-noto-cjk \
|
||||
RUN \
|
||||
apt-get update \
|
||||
# Install dependencies
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
# basic environment
|
||||
curl nodejs libgmp-dev libmpfr-dev libmpc-dev \
|
||||
# For Security
|
||||
# expat libldap-2.5-0 perl libsqlite3-0 zlib1g \
|
||||
# install a chinese font to support the use of tools like matplotlib
|
||||
fonts-noto-cjk \
|
||||
# install libmagic to support the use of python-magic guess MIMETYPE
|
||||
libmagic1 \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
|
@ -80,7 +82,6 @@ COPY . /app/api/
|
|||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
ARG COMMIT_SHA
|
||||
ENV COMMIT_SHA=${COMMIT_SHA}
|
||||
|
||||
|
|
|
|||
|
|
@ -556,6 +556,11 @@ class AuthConfig(BaseSettings):
|
|||
default=86400,
|
||||
)
|
||||
|
||||
FORGOT_PASSWORD_LOCKOUT_DURATION: PositiveInt = Field(
|
||||
description="Time (in seconds) a user must wait before retrying password reset after exceeding the rate limit.",
|
||||
default=86400,
|
||||
)
|
||||
|
||||
|
||||
class ModerationConfig(BaseSettings):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,9 +1,40 @@
|
|||
from typing import Optional
|
||||
|
||||
from pydantic import Field, NonNegativeInt
|
||||
from pydantic import Field, NonNegativeInt, computed_field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class HostedCreditConfig(BaseSettings):
|
||||
HOSTED_MODEL_CREDIT_CONFIG: str = Field(
|
||||
description="Model credit configuration in format 'model:credits,model:credits', e.g., 'gpt-4:20,gpt-4o:10'",
|
||||
default="",
|
||||
)
|
||||
|
||||
def get_model_credits(self, model_name: str) -> int:
|
||||
"""
|
||||
Get credit value for a specific model name.
|
||||
Returns 1 if model is not found in configuration (default credit).
|
||||
|
||||
:param model_name: The name of the model to search for
|
||||
:return: The credit value for the model
|
||||
"""
|
||||
if not self.HOSTED_MODEL_CREDIT_CONFIG:
|
||||
return 1
|
||||
|
||||
try:
|
||||
credit_map = dict(
|
||||
item.strip().split(":", 1) for item in self.HOSTED_MODEL_CREDIT_CONFIG.split(",") if ":" in item
|
||||
)
|
||||
|
||||
# Search for matching model pattern
|
||||
for pattern, credit in credit_map.items():
|
||||
if pattern.strip() == model_name:
|
||||
return int(credit)
|
||||
return 1 # Default quota if no match found
|
||||
except (ValueError, AttributeError):
|
||||
return 1 # Return default quota if parsing fails
|
||||
|
||||
|
||||
class HostedOpenAiConfig(BaseSettings):
|
||||
"""
|
||||
Configuration for hosted OpenAI service
|
||||
|
|
@ -202,5 +233,7 @@ class HostedServiceConfig(
|
|||
HostedZhipuAIConfig,
|
||||
# moderation
|
||||
HostedModerationConfig,
|
||||
# credit config
|
||||
HostedCreditConfig,
|
||||
):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
|||
|
||||
CURRENT_VERSION: str = Field(
|
||||
description="Dify version",
|
||||
default="1.0.0-beta.1",
|
||||
default="1.0.0",
|
||||
)
|
||||
|
||||
COMMIT_SHA: str = Field(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,32 @@
|
|||
import mimetypes
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import urllib.parse
|
||||
import warnings
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
|
||||
try:
|
||||
import magic
|
||||
except ImportError:
|
||||
if platform.system() == "Windows":
|
||||
warnings.warn(
|
||||
"To use python-magic guess MIMETYPE, you need to run `pip install python-magic-bin`", stacklevel=2
|
||||
)
|
||||
elif platform.system() == "Darwin":
|
||||
warnings.warn("To use python-magic guess MIMETYPE, you need to run `brew install libmagic`", stacklevel=2)
|
||||
elif platform.system() == "Linux":
|
||||
warnings.warn(
|
||||
"To use python-magic guess MIMETYPE, you need to run `sudo apt-get install libmagic1`", stacklevel=2
|
||||
)
|
||||
else:
|
||||
warnings.warn("To use python-magic guess MIMETYPE, you need to install `libmagic`", stacklevel=2)
|
||||
magic = None # type: ignore
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from configs import dify_config
|
||||
|
|
@ -47,6 +67,13 @@ def guess_file_info_from_response(response: httpx.Response):
|
|||
# If guessing fails, use Content-Type from response headers
|
||||
mimetype = response.headers.get("Content-Type", "application/octet-stream")
|
||||
|
||||
# Use python-magic to guess MIME type if still unknown or generic
|
||||
if mimetype == "application/octet-stream" and magic is not None:
|
||||
try:
|
||||
mimetype = magic.from_buffer(response.content[:1024], mime=True)
|
||||
except magic.MagicException:
|
||||
pass
|
||||
|
||||
extension = os.path.splitext(filename)[1]
|
||||
|
||||
# Ensure filename has an extension
|
||||
|
|
|
|||
|
|
@ -59,3 +59,9 @@ class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
|
|||
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
||||
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
|
||||
code = 429
|
||||
|
||||
|
||||
class EmailPasswordResetLimitError(BaseHTTPException):
|
||||
error_code = "email_password_reset_limit"
|
||||
description = "Too many failed password reset attempts. Please try again in 24 hours."
|
||||
code = 429
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ from sqlalchemy.orm import Session
|
|||
|
||||
from constants.languages import languages
|
||||
from controllers.console import api
|
||||
from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError
|
||||
from controllers.console.auth.error import (
|
||||
EmailCodeError,
|
||||
EmailPasswordResetLimitError,
|
||||
InvalidEmailError,
|
||||
InvalidTokenError,
|
||||
PasswordMismatchError,
|
||||
)
|
||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
||||
from controllers.console.wraps import setup_required
|
||||
from events.tenant_event import tenant_was_created
|
||||
|
|
@ -65,6 +71,10 @@ class ForgotPasswordCheckApi(Resource):
|
|||
|
||||
user_email = args["email"]
|
||||
|
||||
is_forgot_password_error_rate_limit = AccountService.is_forgot_password_error_rate_limit(args["email"])
|
||||
if is_forgot_password_error_rate_limit:
|
||||
raise EmailPasswordResetLimitError()
|
||||
|
||||
token_data = AccountService.get_reset_password_data(args["token"])
|
||||
if token_data is None:
|
||||
raise InvalidTokenError()
|
||||
|
|
@ -73,8 +83,10 @@ class ForgotPasswordCheckApi(Resource):
|
|||
raise InvalidEmailError()
|
||||
|
||||
if args["code"] != token_data.get("code"):
|
||||
AccountService.add_forgot_password_error_rate_limit(args["email"])
|
||||
raise EmailCodeError()
|
||||
|
||||
AccountService.reset_forgot_password_error_rate_limit(args["email"])
|
||||
return {"is_valid": True, "email": token_data.get("email")}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class MessageListApi(InstalledAppResource):
|
|||
|
||||
try:
|
||||
return MessageService.pagination_by_first_id(
|
||||
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"], "desc"
|
||||
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"]
|
||||
)
|
||||
except services.errors.conversation.ConversationNotExistsError:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import json
|
||||
|
||||
from flask_restful import Resource, reqparse # type: ignore
|
||||
|
||||
from controllers.console.wraps import setup_required
|
||||
|
|
@ -29,4 +31,34 @@ class EnterpriseWorkspace(Resource):
|
|||
return {"message": "enterprise workspace created."}
|
||||
|
||||
|
||||
class EnterpriseWorkspaceNoOwnerEmail(Resource):
|
||||
@setup_required
|
||||
@enterprise_inner_api_only
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("name", type=str, required=True, location="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
tenant = TenantService.create_tenant(args["name"], is_from_dashboard=True)
|
||||
|
||||
tenant_was_created.send(tenant)
|
||||
|
||||
resp = {
|
||||
"id": tenant.id,
|
||||
"name": tenant.name,
|
||||
"encrypt_public_key": tenant.encrypt_public_key,
|
||||
"plan": tenant.plan,
|
||||
"status": tenant.status,
|
||||
"custom_config": json.loads(tenant.custom_config) if tenant.custom_config else {},
|
||||
"created_at": tenant.created_at.isoformat() if tenant.created_at else None,
|
||||
"updated_at": tenant.updated_at.isoformat() if tenant.updated_at else None,
|
||||
}
|
||||
|
||||
return {
|
||||
"message": "enterprise workspace created.",
|
||||
"tenant": resp,
|
||||
}
|
||||
|
||||
|
||||
api.add_resource(EnterpriseWorkspace, "/enterprise/workspace")
|
||||
api.add_resource(EnterpriseWorkspaceNoOwnerEmail, "/enterprise/workspace/ownerless")
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from controllers.service_api.app.error import (
|
|||
from controllers.service_api.dataset.error import (
|
||||
ArchivedDocumentImmutableError,
|
||||
DocumentIndexingError,
|
||||
InvalidMetadataError,
|
||||
)
|
||||
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check
|
||||
from core.errors.error import ProviderTokenNotInitError
|
||||
|
|
@ -50,6 +51,9 @@ class DocumentAddByTextApi(DatasetApiResource):
|
|||
"indexing_technique", type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False, location="json"
|
||||
)
|
||||
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
||||
parser.add_argument("doc_type", type=str, required=False, nullable=True, location="json")
|
||||
parser.add_argument("doc_metadata", type=dict, required=False, nullable=True, location="json")
|
||||
|
||||
args = parser.parse_args()
|
||||
dataset_id = str(dataset_id)
|
||||
tenant_id = str(tenant_id)
|
||||
|
|
@ -61,6 +65,28 @@ class DocumentAddByTextApi(DatasetApiResource):
|
|||
if not dataset.indexing_technique and not args["indexing_technique"]:
|
||||
raise ValueError("indexing_technique is required.")
|
||||
|
||||
# Validate metadata if provided
|
||||
if args.get("doc_type") or args.get("doc_metadata"):
|
||||
if not args.get("doc_type") or not args.get("doc_metadata"):
|
||||
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
||||
|
||||
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
||||
raise InvalidMetadataError(
|
||||
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
||||
)
|
||||
|
||||
if not isinstance(args["doc_metadata"], dict):
|
||||
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
||||
|
||||
# Validate metadata schema based on doc_type
|
||||
if args["doc_type"] != "others":
|
||||
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
||||
for key, value in args["doc_metadata"].items():
|
||||
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
||||
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
||||
# set to MetaDataConfig
|
||||
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
||||
|
||||
text = args.get("text")
|
||||
name = args.get("name")
|
||||
if text is None or name is None:
|
||||
|
|
@ -107,6 +133,8 @@ class DocumentUpdateByTextApi(DatasetApiResource):
|
|||
"doc_language", type=str, default="English", required=False, nullable=False, location="json"
|
||||
)
|
||||
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
||||
parser.add_argument("doc_type", type=str, required=False, nullable=True, location="json")
|
||||
parser.add_argument("doc_metadata", type=dict, required=False, nullable=True, location="json")
|
||||
args = parser.parse_args()
|
||||
dataset_id = str(dataset_id)
|
||||
tenant_id = str(tenant_id)
|
||||
|
|
@ -115,6 +143,32 @@ class DocumentUpdateByTextApi(DatasetApiResource):
|
|||
if not dataset:
|
||||
raise ValueError("Dataset is not exist.")
|
||||
|
||||
# indexing_technique is already set in dataset since this is an update
|
||||
args["indexing_technique"] = dataset.indexing_technique
|
||||
|
||||
# Validate metadata if provided
|
||||
if args.get("doc_type") or args.get("doc_metadata"):
|
||||
if not args.get("doc_type") or not args.get("doc_metadata"):
|
||||
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
||||
|
||||
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
||||
raise InvalidMetadataError(
|
||||
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
||||
)
|
||||
|
||||
if not isinstance(args["doc_metadata"], dict):
|
||||
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
||||
|
||||
# Validate metadata schema based on doc_type
|
||||
if args["doc_type"] != "others":
|
||||
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
||||
for key, value in args["doc_metadata"].items():
|
||||
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
||||
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
||||
|
||||
# set to MetaDataConfig
|
||||
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
||||
|
||||
if args["text"]:
|
||||
text = args.get("text")
|
||||
name = args.get("name")
|
||||
|
|
@ -161,6 +215,30 @@ class DocumentAddByFileApi(DatasetApiResource):
|
|||
args["doc_form"] = "text_model"
|
||||
if "doc_language" not in args:
|
||||
args["doc_language"] = "English"
|
||||
|
||||
# Validate metadata if provided
|
||||
if args.get("doc_type") or args.get("doc_metadata"):
|
||||
if not args.get("doc_type") or not args.get("doc_metadata"):
|
||||
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
||||
|
||||
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
||||
raise InvalidMetadataError(
|
||||
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
||||
)
|
||||
|
||||
if not isinstance(args["doc_metadata"], dict):
|
||||
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
||||
|
||||
# Validate metadata schema based on doc_type
|
||||
if args["doc_type"] != "others":
|
||||
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
||||
for key, value in args["doc_metadata"].items():
|
||||
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
||||
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
||||
|
||||
# set to MetaDataConfig
|
||||
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
||||
|
||||
# get dataset info
|
||||
dataset_id = str(dataset_id)
|
||||
tenant_id = str(tenant_id)
|
||||
|
|
@ -228,6 +306,29 @@ class DocumentUpdateByFileApi(DatasetApiResource):
|
|||
if "doc_language" not in args:
|
||||
args["doc_language"] = "English"
|
||||
|
||||
# Validate metadata if provided
|
||||
if args.get("doc_type") or args.get("doc_metadata"):
|
||||
if not args.get("doc_type") or not args.get("doc_metadata"):
|
||||
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
||||
|
||||
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
||||
raise InvalidMetadataError(
|
||||
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
||||
)
|
||||
|
||||
if not isinstance(args["doc_metadata"], dict):
|
||||
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
||||
|
||||
# Validate metadata schema based on doc_type
|
||||
if args["doc_type"] != "others":
|
||||
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
||||
for key, value in args["doc_metadata"].items():
|
||||
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
||||
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
||||
|
||||
# set to MetaDataConfig
|
||||
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
||||
|
||||
# get dataset info
|
||||
dataset_id = str(dataset_id)
|
||||
tenant_id = str(tenant_id)
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class MessageListApi(WebApiResource):
|
|||
|
||||
try:
|
||||
return MessageService.pagination_by_first_id(
|
||||
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"], "desc"
|
||||
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"]
|
||||
)
|
||||
except services.errors.conversation.ConversationNotExistsError:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@ from core.agent.fc_agent_runner import FunctionCallAgentRunner
|
|||
from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfig
|
||||
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
|
||||
from core.app.apps.base_app_runner import AppRunner
|
||||
from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, ModelConfigWithCredentialsEntity
|
||||
from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity
|
||||
from core.app.entities.queue_entities import QueueAnnotationReplyEvent
|
||||
from core.memory.token_buffer_memory import TokenBufferMemory
|
||||
from core.model_manager import ModelInstance
|
||||
from core.model_runtime.entities.llm_entities import LLMMode, LLMUsage
|
||||
from core.model_runtime.entities.llm_entities import LLMMode
|
||||
from core.model_runtime.entities.model_entities import ModelFeature, ModelPropertyKey
|
||||
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
||||
from core.moderation.base import ModerationError
|
||||
from extensions.ext_database import db
|
||||
from models.model import App, Conversation, Message, MessageAgentThought
|
||||
from models.model import App, Conversation, Message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -191,7 +191,8 @@ class AgentChatAppRunner(AppRunner):
|
|||
# change function call strategy based on LLM model
|
||||
llm_model = cast(LargeLanguageModel, model_instance.model_type_instance)
|
||||
model_schema = llm_model.get_model_schema(model_instance.model, model_instance.credentials)
|
||||
assert model_schema is not None
|
||||
if not model_schema:
|
||||
raise ValueError("Model schema not found")
|
||||
|
||||
if {ModelFeature.MULTI_TOOL_CALL, ModelFeature.TOOL_CALL}.intersection(model_schema.features or []):
|
||||
agent_entity.strategy = AgentEntity.Strategy.FUNCTION_CALLING
|
||||
|
|
@ -247,29 +248,3 @@ class AgentChatAppRunner(AppRunner):
|
|||
stream=application_generate_entity.stream,
|
||||
agent=True,
|
||||
)
|
||||
|
||||
def _get_usage_of_all_agent_thoughts(
|
||||
self, model_config: ModelConfigWithCredentialsEntity, message: Message
|
||||
) -> LLMUsage:
|
||||
"""
|
||||
Get usage of all agent thoughts
|
||||
:param model_config: model config
|
||||
:param message: message
|
||||
:return:
|
||||
"""
|
||||
agent_thoughts = (
|
||||
db.session.query(MessageAgentThought).filter(MessageAgentThought.message_id == message.id).all()
|
||||
)
|
||||
|
||||
all_message_tokens = 0
|
||||
all_answer_tokens = 0
|
||||
for agent_thought in agent_thoughts:
|
||||
all_message_tokens += agent_thought.message_tokens
|
||||
all_answer_tokens += agent_thought.answer_tokens
|
||||
|
||||
model_type_instance = model_config.provider_model_bundle.model_type_instance
|
||||
model_type_instance = cast(LargeLanguageModel, model_type_instance)
|
||||
|
||||
return model_type_instance._calc_response_usage(
|
||||
model_config.model, model_config.credentials, all_message_tokens, all_answer_tokens
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,15 +11,6 @@ from configs import dify_config
|
|||
|
||||
SSRF_DEFAULT_MAX_RETRIES = dify_config.SSRF_DEFAULT_MAX_RETRIES
|
||||
|
||||
proxy_mounts = (
|
||||
{
|
||||
"http://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTP_URL),
|
||||
"https://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTPS_URL),
|
||||
}
|
||||
if dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL
|
||||
else None
|
||||
)
|
||||
|
||||
BACKOFF_FACTOR = 0.5
|
||||
STATUS_FORCELIST = [429, 500, 502, 503, 504]
|
||||
|
||||
|
|
@ -50,7 +41,11 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
|
|||
if dify_config.SSRF_PROXY_ALL_URL:
|
||||
with httpx.Client(proxy=dify_config.SSRF_PROXY_ALL_URL) as client:
|
||||
response = client.request(method=method, url=url, **kwargs)
|
||||
elif proxy_mounts:
|
||||
elif dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL:
|
||||
proxy_mounts = {
|
||||
"http://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTP_URL),
|
||||
"https://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTPS_URL),
|
||||
}
|
||||
with httpx.Client(mounts=proxy_mounts) as client:
|
||||
response = client.request(method=method, url=url, **kwargs)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage
|
||||
from .llm_entities import LLMMode, LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage
|
||||
from .message_entities import (
|
||||
AssistantPromptMessage,
|
||||
AudioPromptMessageContent,
|
||||
|
|
@ -23,6 +23,7 @@ __all__ = [
|
|||
"AudioPromptMessageContent",
|
||||
"DocumentPromptMessageContent",
|
||||
"ImagePromptMessageContent",
|
||||
"LLMMode",
|
||||
"LLMResult",
|
||||
"LLMResultChunk",
|
||||
"LLMResultChunkDelta",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -8,7 +8,7 @@ from core.model_runtime.entities.message_entities import AssistantPromptMessage,
|
|||
from core.model_runtime.entities.model_entities import ModelUsage, PriceInfo
|
||||
|
||||
|
||||
class LLMMode(Enum):
|
||||
class LLMMode(StrEnum):
|
||||
"""
|
||||
Enum class for large language model mode.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ from typing import Optional
|
|||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from core.model_runtime.entities.common_entities import I18nObject
|
||||
from core.model_runtime.entities.defaults import PARAMETER_RULE_TEMPLATE
|
||||
from core.model_runtime.entities.model_entities import (
|
||||
AIModelEntity,
|
||||
DefaultParameterName,
|
||||
ModelType,
|
||||
PriceConfig,
|
||||
PriceInfo,
|
||||
|
|
@ -18,6 +21,7 @@ from core.model_runtime.errors.invoke import (
|
|||
InvokeRateLimitError,
|
||||
InvokeServerUnavailableError,
|
||||
)
|
||||
from core.model_runtime.model_providers.__base.tokenizers.gpt2_tokenzier import GPT2Tokenizer
|
||||
from core.plugin.entities.plugin_daemon import PluginDaemonInnerError, PluginModelProviderEntity
|
||||
from core.plugin.manager.model import PluginModelManager
|
||||
|
||||
|
|
@ -144,3 +148,102 @@ class AIModel(BaseModel):
|
|||
model=model,
|
||||
credentials=credentials or {},
|
||||
)
|
||||
|
||||
def get_customizable_model_schema_from_credentials(self, model: str, credentials: dict) -> Optional[AIModelEntity]:
|
||||
"""
|
||||
Get customizable model schema from credentials
|
||||
|
||||
:param model: model name
|
||||
:param credentials: model credentials
|
||||
:return: model schema
|
||||
"""
|
||||
return self._get_customizable_model_schema(model, credentials)
|
||||
|
||||
def _get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]:
|
||||
"""
|
||||
Get customizable model schema and fill in the template
|
||||
"""
|
||||
schema = self.get_customizable_model_schema(model, credentials)
|
||||
|
||||
if not schema:
|
||||
return None
|
||||
|
||||
# fill in the template
|
||||
new_parameter_rules = []
|
||||
for parameter_rule in schema.parameter_rules:
|
||||
if parameter_rule.use_template:
|
||||
try:
|
||||
default_parameter_name = DefaultParameterName.value_of(parameter_rule.use_template)
|
||||
default_parameter_rule = self._get_default_parameter_rule_variable_map(default_parameter_name)
|
||||
if not parameter_rule.max and "max" in default_parameter_rule:
|
||||
parameter_rule.max = default_parameter_rule["max"]
|
||||
if not parameter_rule.min and "min" in default_parameter_rule:
|
||||
parameter_rule.min = default_parameter_rule["min"]
|
||||
if not parameter_rule.default and "default" in default_parameter_rule:
|
||||
parameter_rule.default = default_parameter_rule["default"]
|
||||
if not parameter_rule.precision and "precision" in default_parameter_rule:
|
||||
parameter_rule.precision = default_parameter_rule["precision"]
|
||||
if not parameter_rule.required and "required" in default_parameter_rule:
|
||||
parameter_rule.required = default_parameter_rule["required"]
|
||||
if not parameter_rule.help and "help" in default_parameter_rule:
|
||||
parameter_rule.help = I18nObject(
|
||||
en_US=default_parameter_rule["help"]["en_US"],
|
||||
)
|
||||
if (
|
||||
parameter_rule.help
|
||||
and not parameter_rule.help.en_US
|
||||
and ("help" in default_parameter_rule and "en_US" in default_parameter_rule["help"])
|
||||
):
|
||||
parameter_rule.help.en_US = default_parameter_rule["help"]["en_US"]
|
||||
if (
|
||||
parameter_rule.help
|
||||
and not parameter_rule.help.zh_Hans
|
||||
and ("help" in default_parameter_rule and "zh_Hans" in default_parameter_rule["help"])
|
||||
):
|
||||
parameter_rule.help.zh_Hans = default_parameter_rule["help"].get(
|
||||
"zh_Hans", default_parameter_rule["help"]["en_US"]
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
new_parameter_rules.append(parameter_rule)
|
||||
|
||||
schema.parameter_rules = new_parameter_rules
|
||||
|
||||
return schema
|
||||
|
||||
def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]:
|
||||
"""
|
||||
Get customizable model schema
|
||||
|
||||
:param model: model name
|
||||
:param credentials: model credentials
|
||||
:return: model schema
|
||||
"""
|
||||
return None
|
||||
|
||||
def _get_default_parameter_rule_variable_map(self, name: DefaultParameterName) -> dict:
|
||||
"""
|
||||
Get default parameter rule for given name
|
||||
|
||||
:param name: parameter name
|
||||
:return: parameter rule
|
||||
"""
|
||||
default_parameter_rule = PARAMETER_RULE_TEMPLATE.get(name)
|
||||
|
||||
if not default_parameter_rule:
|
||||
raise Exception(f"Invalid model parameter rule name {name}")
|
||||
|
||||
return default_parameter_rule
|
||||
|
||||
def _get_num_tokens_by_gpt2(self, text: str) -> int:
|
||||
"""
|
||||
Get number of tokens for given prompt messages by gpt2
|
||||
Some provider models do not provide an interface for obtaining the number of tokens.
|
||||
Here, the gpt2 tokenizer is used to calculate the number of tokens.
|
||||
This method can be executed offline, and the gpt2 tokenizer has been cached in the project.
|
||||
|
||||
:param text: plain text of prompt. You need to convert the original message to plain text
|
||||
:return: number of tokens
|
||||
"""
|
||||
return GPT2Tokenizer.get_num_tokens(text)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
- openai
|
||||
- deepseek
|
||||
- anthropic
|
||||
- azure_openai
|
||||
- google
|
||||
|
|
@ -32,7 +33,6 @@
|
|||
- localai
|
||||
- volcengine_maas
|
||||
- openai_api_compatible
|
||||
- deepseek
|
||||
- hunyuan
|
||||
- siliconflow
|
||||
- perfxcloud
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
model: gemini-2.0-flash-001
|
||||
label:
|
||||
en_US: Gemini 2.0 Flash 001
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- document
|
||||
- video
|
||||
- audio
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 1048576
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
model: gemini-2.0-flash-lite-preview-02-05
|
||||
label:
|
||||
en_US: Gemini 2.0 Flash Lite Preview 0205
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- document
|
||||
- video
|
||||
- audio
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 1048576
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
model: gemini-2.0-flash-thinking-exp-01-21
|
||||
label:
|
||||
en_US: Gemini 2.0 Flash Thinking Exp 0121
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- document
|
||||
- video
|
||||
- audio
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 32767
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
model: gemini-2.0-flash-thinking-exp-1219
|
||||
label:
|
||||
en_US: Gemini 2.0 Flash Thinking Exp 1219
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- document
|
||||
- video
|
||||
- audio
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 32767
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
model: gemini-2.0-pro-exp-02-05
|
||||
label:
|
||||
en_US: Gemini 2.0 Pro Exp 0205
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- document
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 2000000
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: presence_penalty
|
||||
use_template: presence_penalty
|
||||
- name: frequency_penalty
|
||||
use_template: frequency_penalty
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
model: gemini-exp-1114
|
||||
label:
|
||||
en_US: Gemini exp 1114
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- document
|
||||
- video
|
||||
- audio
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 32767
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
model: gemini-exp-1121
|
||||
label:
|
||||
en_US: Gemini exp 1121
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- document
|
||||
- video
|
||||
- audio
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 32767
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
model: gemini-exp-1206
|
||||
label:
|
||||
en_US: Gemini exp 1206
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- document
|
||||
- video
|
||||
- audio
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 2097152
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
model: glm-4-air-0111
|
||||
label:
|
||||
en_US: glm-4-air-0111
|
||||
model_type: llm
|
||||
features:
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 131072
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
default: 0.95
|
||||
min: 0.0
|
||||
max: 1.0
|
||||
help:
|
||||
zh_Hans: 采样温度,控制输出的随机性,必须为正数取值范围是:(0.0,1.0],不能等于 0,默认值为 0.95 值越大,会使输出更随机,更具创造性;值越小,输出会更加稳定或确定建议您根据应用场景调整 top_p 或 temperature 参数,但不要同时调整两个参数。
|
||||
en_US: Sampling temperature, controls the randomness of the output, must be a positive number. The value range is (0.0,1.0], which cannot be equal to 0. The default value is 0.95. The larger the value, the more random and creative the output will be; the smaller the value, The output will be more stable or certain. It is recommended that you adjust the top_p or temperature parameters according to the application scenario, but do not adjust both parameters at the same time.
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
default: 0.7
|
||||
help:
|
||||
zh_Hans: 用温度取样的另一种方法,称为核取样取值范围是:(0.0, 1.0) 开区间,不能等于 0 或 1,默认值为 0.7 模型考虑具有 top_p 概率质量tokens的结果例如:0.1 意味着模型解码器只考虑从前 10% 的概率的候选集中取 tokens 建议您根据应用场景调整 top_p 或 temperature 参数,但不要同时调整两个参数。
|
||||
en_US: Another method of temperature sampling is called kernel sampling. The value range is (0.0, 1.0) open interval, which cannot be equal to 0 or 1. The default value is 0.7. The model considers the results with top_p probability mass tokens. For example 0.1 means The model decoder only considers tokens from the candidate set with the top 10% probability. It is recommended that you adjust the top_p or temperature parameters according to the application scenario, but do not adjust both parameters at the same time.
|
||||
- name: do_sample
|
||||
label:
|
||||
zh_Hans: 采样策略
|
||||
en_US: Sampling strategy
|
||||
type: boolean
|
||||
help:
|
||||
zh_Hans: do_sample 为 true 时启用采样策略,do_sample 为 false 时采样策略 temperature、top_p 将不生效。默认值为 true。
|
||||
en_US: When `do_sample` is set to true, the sampling strategy is enabled. When `do_sample` is set to false, the sampling strategies such as `temperature` and `top_p` will not take effect. The default value is true.
|
||||
default: true
|
||||
- name: max_tokens
|
||||
use_template: max_tokens
|
||||
default: 1024
|
||||
min: 1
|
||||
max: 4095
|
||||
- name: web_search
|
||||
type: boolean
|
||||
label:
|
||||
zh_Hans: 联网搜索
|
||||
en_US: Web Search
|
||||
default: false
|
||||
help:
|
||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
||||
- name: response_format
|
||||
label:
|
||||
zh_Hans: 回复格式
|
||||
en_US: Response Format
|
||||
type: string
|
||||
help:
|
||||
zh_Hans: 指定模型必须输出的格式
|
||||
en_US: specifying the format that the model must output
|
||||
required: false
|
||||
options:
|
||||
- text
|
||||
- json_object
|
||||
pricing:
|
||||
input: '0.0005'
|
||||
output: '0.0005'
|
||||
unit: '0.001'
|
||||
currency: RMB
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
import time
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
import requests
|
||||
|
||||
|
|
@ -14,48 +14,47 @@ class FirecrawlApp:
|
|||
if self.api_key is None and self.base_url == "https://api.firecrawl.dev":
|
||||
raise ValueError("No API key provided")
|
||||
|
||||
def scrape_url(self, url, params=None) -> dict:
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}"}
|
||||
json_data = {"url": url}
|
||||
def scrape_url(self, url, params=None) -> dict[str, Any]:
|
||||
# Documentation: https://docs.firecrawl.dev/api-reference/endpoint/scrape
|
||||
headers = self._prepare_headers()
|
||||
json_data = {
|
||||
"url": url,
|
||||
"formats": ["markdown"],
|
||||
"onlyMainContent": True,
|
||||
"timeout": 30000,
|
||||
}
|
||||
if params:
|
||||
json_data.update(params)
|
||||
response = requests.post(f"{self.base_url}/v0/scrape", headers=headers, json=json_data)
|
||||
response = self._post_request(f"{self.base_url}/v1/scrape", json_data, headers)
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
if response_data["success"] == True:
|
||||
data = response_data["data"]
|
||||
return {
|
||||
"title": data.get("metadata").get("title"),
|
||||
"description": data.get("metadata").get("description"),
|
||||
"source_url": data.get("metadata").get("sourceURL"),
|
||||
"markdown": data.get("markdown"),
|
||||
}
|
||||
else:
|
||||
raise Exception(f"Failed to scrape URL. Error: {response_data['error']}")
|
||||
|
||||
elif response.status_code in {402, 409, 500}:
|
||||
error_message = response.json().get("error", "Unknown error occurred")
|
||||
raise Exception(f"Failed to scrape URL. Status code: {response.status_code}. Error: {error_message}")
|
||||
data = response_data["data"]
|
||||
return self._extract_common_fields(data)
|
||||
elif response.status_code in {402, 409, 500, 429, 408}:
|
||||
self._handle_error(response, "scrape URL")
|
||||
return {} # Avoid additional exception after handling error
|
||||
else:
|
||||
raise Exception(f"Failed to scrape URL. Status code: {response.status_code}")
|
||||
|
||||
def crawl_url(self, url, params=None) -> str:
|
||||
# Documentation: https://docs.firecrawl.dev/api-reference/endpoint/crawl-post
|
||||
headers = self._prepare_headers()
|
||||
json_data = {"url": url}
|
||||
if params:
|
||||
json_data.update(params)
|
||||
response = self._post_request(f"{self.base_url}/v0/crawl", json_data, headers)
|
||||
response = self._post_request(f"{self.base_url}/v1/crawl", json_data, headers)
|
||||
if response.status_code == 200:
|
||||
job_id = response.json().get("jobId")
|
||||
# There's also another two fields in the response: "success" (bool) and "url" (str)
|
||||
job_id = response.json().get("id")
|
||||
return cast(str, job_id)
|
||||
else:
|
||||
self._handle_error(response, "start crawl job")
|
||||
# FIXME: unreachable code for mypy
|
||||
return "" # unreachable
|
||||
|
||||
def check_crawl_status(self, job_id) -> dict:
|
||||
def check_crawl_status(self, job_id) -> dict[str, Any]:
|
||||
headers = self._prepare_headers()
|
||||
response = self._get_request(f"{self.base_url}/v0/crawl/status/{job_id}", headers)
|
||||
response = self._get_request(f"{self.base_url}/v1/crawl/{job_id}", headers)
|
||||
if response.status_code == 200:
|
||||
crawl_status_response = response.json()
|
||||
if crawl_status_response.get("status") == "completed":
|
||||
|
|
@ -66,42 +65,48 @@ class FirecrawlApp:
|
|||
url_data_list = []
|
||||
for item in data:
|
||||
if isinstance(item, dict) and "metadata" in item and "markdown" in item:
|
||||
url_data = {
|
||||
"title": item.get("metadata", {}).get("title"),
|
||||
"description": item.get("metadata", {}).get("description"),
|
||||
"source_url": item.get("metadata", {}).get("sourceURL"),
|
||||
"markdown": item.get("markdown"),
|
||||
}
|
||||
url_data = self._extract_common_fields(item)
|
||||
url_data_list.append(url_data)
|
||||
if url_data_list:
|
||||
file_key = "website_files/" + job_id + ".txt"
|
||||
if storage.exists(file_key):
|
||||
storage.delete(file_key)
|
||||
storage.save(file_key, json.dumps(url_data_list).encode("utf-8"))
|
||||
return {
|
||||
"status": "completed",
|
||||
"total": crawl_status_response.get("total"),
|
||||
"current": crawl_status_response.get("current"),
|
||||
"data": url_data_list,
|
||||
}
|
||||
|
||||
try:
|
||||
if storage.exists(file_key):
|
||||
storage.delete(file_key)
|
||||
storage.save(file_key, json.dumps(url_data_list).encode("utf-8"))
|
||||
except Exception as e:
|
||||
raise Exception(f"Error saving crawl data: {e}")
|
||||
return self._format_crawl_status_response("completed", crawl_status_response, url_data_list)
|
||||
else:
|
||||
return {
|
||||
"status": crawl_status_response.get("status"),
|
||||
"total": crawl_status_response.get("total"),
|
||||
"current": crawl_status_response.get("current"),
|
||||
"data": [],
|
||||
}
|
||||
|
||||
return self._format_crawl_status_response(
|
||||
crawl_status_response.get("status"), crawl_status_response, []
|
||||
)
|
||||
else:
|
||||
self._handle_error(response, "check crawl status")
|
||||
# FIXME: unreachable code for mypy
|
||||
return {} # unreachable
|
||||
|
||||
def _prepare_headers(self):
|
||||
def _format_crawl_status_response(
|
||||
self, status: str, crawl_status_response: dict[str, Any], url_data_list: list[dict[str, Any]]
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"status": status,
|
||||
"total": crawl_status_response.get("total"),
|
||||
"current": crawl_status_response.get("completed"),
|
||||
"data": url_data_list,
|
||||
}
|
||||
|
||||
def _extract_common_fields(self, item: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"title": item.get("metadata", {}).get("title"),
|
||||
"description": item.get("metadata", {}).get("description"),
|
||||
"source_url": item.get("metadata", {}).get("sourceURL"),
|
||||
"markdown": item.get("markdown"),
|
||||
}
|
||||
|
||||
def _prepare_headers(self) -> dict[str, Any]:
|
||||
return {"Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}"}
|
||||
|
||||
def _post_request(self, url, data, headers, retries=3, backoff_factor=0.5):
|
||||
def _post_request(self, url, data, headers, retries=3, backoff_factor=0.5) -> requests.Response:
|
||||
for attempt in range(retries):
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
if response.status_code == 502:
|
||||
|
|
@ -110,7 +115,7 @@ class FirecrawlApp:
|
|||
return response
|
||||
return response
|
||||
|
||||
def _get_request(self, url, headers, retries=3, backoff_factor=0.5):
|
||||
def _get_request(self, url, headers, retries=3, backoff_factor=0.5) -> requests.Response:
|
||||
for attempt in range(retries):
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 502:
|
||||
|
|
@ -119,6 +124,6 @@ class FirecrawlApp:
|
|||
return response
|
||||
return response
|
||||
|
||||
def _handle_error(self, response, action):
|
||||
def _handle_error(self, response, action) -> None:
|
||||
error_message = response.json().get("error", "Unknown error occurred")
|
||||
raise Exception(f"Failed to {action}. Status code: {response.status_code}. Error: {error_message}")
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ class FirecrawlWebExtractor(BaseExtractor):
|
|||
api_key: The API key for Firecrawl.
|
||||
base_url: The base URL for the Firecrawl API. Defaults to 'https://api.firecrawl.dev'.
|
||||
mode: The mode of operation. Defaults to 'scrape'. Options are 'crawl', 'scrape' and 'crawl_return_urls'.
|
||||
only_main_content: Only return the main content of the page excluding headers, navs, footers, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, url: str, job_id: str, tenant_id: str, mode: str = "crawl", only_main_content: bool = False):
|
||||
def __init__(self, url: str, job_id: str, tenant_id: str, mode: str = "crawl", only_main_content: bool = True):
|
||||
"""Initialize with url, api_key, base_url and mode."""
|
||||
self._url = url
|
||||
self.job_id = job_id
|
||||
|
|
|
|||
|
|
@ -223,14 +223,14 @@ class WorkflowTool(Tool):
|
|||
if isinstance(value, list):
|
||||
for item in value:
|
||||
if isinstance(item, dict) and item.get("dify_model_identity") == FILE_MODEL_IDENTITY:
|
||||
item["tool_file_id"] = item.get("related_id")
|
||||
item = self._update_file_mapping(item)
|
||||
file = build_from_mapping(
|
||||
mapping=item,
|
||||
tenant_id=str(cast(ToolRuntime, self.runtime).tenant_id),
|
||||
)
|
||||
files.append(file)
|
||||
elif isinstance(value, dict) and value.get("dify_model_identity") == FILE_MODEL_IDENTITY:
|
||||
value["tool_file_id"] = value.get("related_id")
|
||||
value = self._update_file_mapping(value)
|
||||
file = build_from_mapping(
|
||||
mapping=value,
|
||||
tenant_id=str(cast(ToolRuntime, self.runtime).tenant_id),
|
||||
|
|
@ -240,3 +240,11 @@ class WorkflowTool(Tool):
|
|||
result[key] = value
|
||||
|
||||
return result, files
|
||||
|
||||
def _update_file_mapping(self, file_dict: dict) -> dict:
|
||||
transfer_method = FileTransferMethod.value_of(file_dict.get("transfer_method"))
|
||||
if transfer_method == FileTransferMethod.TOOL_FILE:
|
||||
file_dict["tool_file_id"] = file_dict.get("related_id")
|
||||
elif transfer_method == FileTransferMethod.LOCAL_FILE:
|
||||
file_dict["upload_file_id"] = file_dict.get("related_id")
|
||||
return file_dict
|
||||
|
|
|
|||
|
|
@ -590,6 +590,8 @@ class Graph(BaseModel):
|
|||
start_node_id=node_id,
|
||||
routes_node_ids=routes_node_ids,
|
||||
)
|
||||
# Exclude conditional branch nodes
|
||||
and all(edge.run_condition is None for edge in reverse_edge_mapping.get(node_id, []))
|
||||
):
|
||||
if node_id not in merge_branch_node_ids:
|
||||
merge_branch_node_ids[node_id] = []
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
|||
if output_config.type == "object":
|
||||
# check if output is object
|
||||
if not isinstance(result.get(output_name), dict):
|
||||
if isinstance(result.get(output_name), type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
|
|
@ -223,7 +223,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
|||
elif output_config.type == "array[number]":
|
||||
# check if array of number available
|
||||
if not isinstance(result[output_name], list):
|
||||
if isinstance(result[output_name], type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
|
|
@ -244,7 +244,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
|||
elif output_config.type == "array[string]":
|
||||
# check if array of string available
|
||||
if not isinstance(result[output_name], list):
|
||||
if isinstance(result[output_name], type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
|
|
@ -265,7 +265,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
|||
elif output_config.type == "array[object]":
|
||||
# check if array of object available
|
||||
if not isinstance(result[output_name], list):
|
||||
if isinstance(result[output_name], type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Any, Optional
|
|||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from core.model_runtime.entities import ImagePromptMessageContent
|
||||
from core.model_runtime.entities import ImagePromptMessageContent, LLMMode
|
||||
from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig
|
||||
from core.workflow.entities.variable_entities import VariableSelector
|
||||
from core.workflow.nodes.base import BaseNodeData
|
||||
|
|
@ -12,7 +12,7 @@ from core.workflow.nodes.base import BaseNodeData
|
|||
class ModelConfig(BaseModel):
|
||||
provider: str
|
||||
name: str
|
||||
mode: str
|
||||
mode: LLMMode
|
||||
completion_params: dict[str, Any] = {}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import logging
|
|||
from collections.abc import Generator, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Optional, cast
|
||||
|
||||
from configs import dify_config
|
||||
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
||||
from core.entities.model_entities import ModelStatus
|
||||
from core.entities.provider_entities import QuotaUnit
|
||||
|
|
@ -185,6 +186,8 @@ class LLMNode(BaseNode[LLMNodeData]):
|
|||
result_text = event.text
|
||||
usage = event.usage
|
||||
finish_reason = event.finish_reason
|
||||
# deduct quota
|
||||
self.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
|
||||
break
|
||||
except LLMNodeError as e:
|
||||
yield RunCompletedEvent(
|
||||
|
|
@ -240,17 +243,7 @@ class LLMNode(BaseNode[LLMNodeData]):
|
|||
user=self.user_id,
|
||||
)
|
||||
|
||||
# handle invoke result
|
||||
generator = self._handle_invoke_result(invoke_result=invoke_result)
|
||||
|
||||
usage = LLMUsage.empty_usage()
|
||||
for event in generator:
|
||||
yield event
|
||||
if isinstance(event, ModelInvokeCompletedEvent):
|
||||
usage = event.usage
|
||||
|
||||
# deduct quota
|
||||
self.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
|
||||
return self._handle_invoke_result(invoke_result=invoke_result)
|
||||
|
||||
def _handle_invoke_result(self, invoke_result: LLMResult | Generator) -> Generator[NodeEvent, None, None]:
|
||||
if isinstance(invoke_result, LLMResult):
|
||||
|
|
@ -740,10 +733,7 @@ class LLMNode(BaseNode[LLMNodeData]):
|
|||
if quota_unit == QuotaUnit.TOKENS:
|
||||
used_quota = usage.total_tokens
|
||||
elif quota_unit == QuotaUnit.CREDITS:
|
||||
used_quota = 1
|
||||
|
||||
if "gpt-4" in model_instance.model:
|
||||
used_quota = 20
|
||||
used_quota = dify_config.get_model_credits(model_instance.model)
|
||||
else:
|
||||
used_quota = 1
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ if [[ "${MODE}" == "worker" ]]; then
|
|||
CONCURRENCY_OPTION="-c ${CELERY_WORKER_AMOUNT:-1}"
|
||||
fi
|
||||
|
||||
exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION --loglevel ${LOG_LEVEL} \
|
||||
exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION --loglevel ${LOG_LEVEL:-INFO} \
|
||||
-Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion}
|
||||
|
||||
elif [[ "${MODE}" == "beat" ]]; then
|
||||
exec celery -A app.celery beat --loglevel ${LOG_LEVEL}
|
||||
exec celery -A app.celery beat --loglevel ${LOG_LEVEL:-INFO}
|
||||
else
|
||||
if [[ "${DEBUG}" == "true" ]]; then
|
||||
exec flask run --host=${DIFY_BIND_ADDRESS:-0.0.0.0} --port=${DIFY_PORT:-5001} --debug
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from configs import dify_config
|
||||
from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, ChatAppGenerateEntity
|
||||
from core.entities.provider_entities import QuotaUnit
|
||||
from events.message_event import message_was_created
|
||||
|
|
@ -40,10 +41,7 @@ def handle(sender, **kwargs):
|
|||
if quota_unit == QuotaUnit.TOKENS:
|
||||
used_quota = message.message_tokens + message.answer_tokens
|
||||
elif quota_unit == QuotaUnit.CREDITS:
|
||||
used_quota = 1
|
||||
|
||||
if "gpt-4" in model_config.model:
|
||||
used_quota = 20
|
||||
used_quota = dify_config.get_model_credits(model_config.model)
|
||||
else:
|
||||
used_quota = 1
|
||||
|
||||
|
|
|
|||
|
|
@ -27,12 +27,11 @@ def init_app(app: DifyApp):
|
|||
# Always add StreamHandler to log to console
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.addFilter(RequestIdFilter())
|
||||
log_formatter = logging.Formatter(fmt=dify_config.LOG_FORMAT)
|
||||
sh.setFormatter(log_formatter)
|
||||
log_handlers.append(sh)
|
||||
|
||||
logging.basicConfig(
|
||||
level=dify_config.LOG_LEVEL,
|
||||
format=dify_config.LOG_FORMAT,
|
||||
datefmt=dify_config.LOG_DATEFORMAT,
|
||||
handlers=log_handlers,
|
||||
force=True,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ class AwsS3Storage(BaseStorage):
|
|||
aws_access_key_id=dify_config.S3_ACCESS_KEY,
|
||||
endpoint_url=dify_config.S3_ENDPOINT,
|
||||
region_name=dify_config.S3_REGION,
|
||||
config=Config(s3={"addressing_style": dify_config.S3_ADDRESS_STYLE}),
|
||||
config=Config(
|
||||
s3={"addressing_style": dify_config.S3_ADDRESS_STYLE},
|
||||
request_checksum_calculation="when_required",
|
||||
response_checksum_validation="when_required",
|
||||
),
|
||||
)
|
||||
# create bucket
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from collections.abc import Generator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from azure.identity import ChainedTokenCredential, DefaultAzureCredential
|
||||
from azure.storage.blob import AccountSasPermissions, BlobServiceClient, ResourceTypes, generate_account_sas
|
||||
|
||||
from configs import dify_config
|
||||
|
|
@ -18,6 +20,12 @@ class AzureBlobStorage(BaseStorage):
|
|||
self.account_name = dify_config.AZURE_BLOB_ACCOUNT_NAME
|
||||
self.account_key = dify_config.AZURE_BLOB_ACCOUNT_KEY
|
||||
|
||||
self.credential: Optional[ChainedTokenCredential] = None
|
||||
if self.account_key == "managedidentity":
|
||||
self.credential = DefaultAzureCredential()
|
||||
else:
|
||||
self.credential = None
|
||||
|
||||
def save(self, filename, data):
|
||||
client = self._sync_client()
|
||||
blob_container = client.get_container_client(container=self.bucket_name)
|
||||
|
|
@ -57,6 +65,9 @@ class AzureBlobStorage(BaseStorage):
|
|||
blob_container.delete_blob(filename)
|
||||
|
||||
def _sync_client(self):
|
||||
if self.account_key == "managedidentity":
|
||||
return BlobServiceClient(account_url=self.account_url, credential=self.credential) # type: ignore
|
||||
|
||||
cache_key = "azure_blob_sas_token_{}_{}".format(self.account_name, self.account_key)
|
||||
cache_result = redis_client.get(cache_key)
|
||||
if cache_result is not None:
|
||||
|
|
|
|||
|
|
@ -1147,8 +1147,10 @@ class Message(Base):
|
|||
"id": self.id,
|
||||
"app_id": self.app_id,
|
||||
"conversation_id": self.conversation_id,
|
||||
"model_id": self.model_id,
|
||||
"inputs": self.inputs,
|
||||
"query": self.query,
|
||||
"total_price": self.total_price,
|
||||
"message": self.message,
|
||||
"answer": self.answer,
|
||||
"status": self.status,
|
||||
|
|
@ -1169,7 +1171,9 @@ class Message(Base):
|
|||
id=data["id"],
|
||||
app_id=data["app_id"],
|
||||
conversation_id=data["conversation_id"],
|
||||
model_id=data["model_id"],
|
||||
inputs=data["inputs"],
|
||||
total_price=data["total_price"],
|
||||
query=data["query"],
|
||||
message=data["message"],
|
||||
answer=data["answer"],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,7 +18,7 @@ package-mode = false
|
|||
authlib = "1.3.1"
|
||||
azure-identity = "1.16.1"
|
||||
beautifulsoup4 = "4.12.2"
|
||||
boto3 = "1.35.74"
|
||||
boto3 = "1.36.12"
|
||||
bs4 = "~0.0.1"
|
||||
cachetools = "~5.3.0"
|
||||
celery = "~5.4.0"
|
||||
|
|
@ -47,7 +47,7 @@ mailchimp-transactional = "~1.0.50"
|
|||
markdown = "~3.5.1"
|
||||
numpy = "~1.26.4"
|
||||
oci = "~2.135.1"
|
||||
openai = "~1.52.0"
|
||||
openai = "~1.61.0"
|
||||
openpyxl = "~3.1.5"
|
||||
opik = "~1.3.4"
|
||||
pandas = { version = "~2.2.2", extras = ["performance", "excel"] }
|
||||
|
|
@ -73,7 +73,6 @@ starlette = "0.41.0"
|
|||
tiktoken = "~0.8.0"
|
||||
tokenizers = "~0.15.0"
|
||||
transformers = "~4.35.0"
|
||||
types-pytz = "~2024.2.0.20241003"
|
||||
unstructured = { version = "~0.16.1", extras = ["docx", "epub", "md", "msg", "ppt", "pptx"] }
|
||||
validators = "0.21.0"
|
||||
yarl = "~1.18.3"
|
||||
|
|
@ -150,6 +149,21 @@ pytest = "~8.3.2"
|
|||
pytest-benchmark = "~4.0.0"
|
||||
pytest-env = "~1.1.3"
|
||||
pytest-mock = "~3.14.0"
|
||||
types-beautifulsoup4 = "~4.12.0.20241020"
|
||||
types-flask-cors = "~5.0.0.20240902"
|
||||
types-flask-migrate = "~4.1.0.20250112"
|
||||
types-html5lib = "~1.1.11.20241018"
|
||||
types-openpyxl = "~3.1.5.20241225"
|
||||
types-protobuf = "~5.29.1.20241207"
|
||||
types-psutil = "~6.1.0.20241221"
|
||||
types-psycopg2 = "~2.9.21.20250121"
|
||||
types-python-dateutil = "~2.9.0.20241206"
|
||||
types-pytz = "~2024.2.0.20241221"
|
||||
types-pyyaml = "~6.0.12.20241230"
|
||||
types-regex = "~2024.11.6.20241221"
|
||||
types-requests = "~2.32.0.20241016"
|
||||
types-six = "~1.17.0.20241205"
|
||||
types-tqdm = "~4.67.0.20241221"
|
||||
|
||||
############################################################
|
||||
# [ Lint ] dependency group
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ class AccountService:
|
|||
prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1
|
||||
)
|
||||
LOGIN_MAX_ERROR_LIMITS = 5
|
||||
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
|
||||
|
||||
@staticmethod
|
||||
def _get_refresh_token_key(refresh_token: str) -> str:
|
||||
|
|
@ -504,6 +505,32 @@ class AccountService:
|
|||
key = f"login_error_rate_limit:{email}"
|
||||
redis_client.delete(key)
|
||||
|
||||
@staticmethod
|
||||
def add_forgot_password_error_rate_limit(email: str) -> None:
|
||||
key = f"forgot_password_error_rate_limit:{email}"
|
||||
count = redis_client.get(key)
|
||||
if count is None:
|
||||
count = 0
|
||||
count = int(count) + 1
|
||||
redis_client.setex(key, dify_config.FORGOT_PASSWORD_LOCKOUT_DURATION, count)
|
||||
|
||||
@staticmethod
|
||||
def is_forgot_password_error_rate_limit(email: str) -> bool:
|
||||
key = f"forgot_password_error_rate_limit:{email}"
|
||||
count = redis_client.get(key)
|
||||
if count is None:
|
||||
return False
|
||||
|
||||
count = int(count)
|
||||
if count > AccountService.FORGOT_PASSWORD_MAX_ERROR_LIMITS:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def reset_forgot_password_error_rate_limit(email: str):
|
||||
key = f"forgot_password_error_rate_limit:{email}"
|
||||
redis_client.delete(key)
|
||||
|
||||
@staticmethod
|
||||
def is_email_send_ip_limit(ip_address: str):
|
||||
minute_key = f"email_send_ip_limit_minute:{ip_address}"
|
||||
|
|
|
|||
|
|
@ -21,10 +21,12 @@ class FirecrawlAuth(ApiKeyAuthBase):
|
|||
headers = self._prepare_headers()
|
||||
options = {
|
||||
"url": "https://example.com",
|
||||
"crawlerOptions": {"excludes": [], "includes": [], "limit": 1},
|
||||
"pageOptions": {"onlyMainContent": True},
|
||||
"includePaths": [],
|
||||
"excludePaths": [],
|
||||
"limit": 1,
|
||||
"scrapeOptions": {"onlyMainContent": True},
|
||||
}
|
||||
response = self._post_request(f"{self.base_url}/v0/crawl", options, headers)
|
||||
response = self._post_request(f"{self.base_url}/v1/crawl", options, headers)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ from models.source import DataSourceOauthBinding
|
|||
from services.entities.knowledge_entities.knowledge_entities import (
|
||||
ChildChunkUpdateArgs,
|
||||
KnowledgeConfig,
|
||||
MetaDataConfig,
|
||||
RerankingModel,
|
||||
RetrievalModel,
|
||||
SegmentUpdateArgs,
|
||||
|
|
@ -915,6 +916,9 @@ class DocumentService:
|
|||
document.data_source_info = json.dumps(data_source_info)
|
||||
document.batch = batch
|
||||
document.indexing_status = "waiting"
|
||||
if knowledge_config.metadata:
|
||||
document.doc_type = knowledge_config.metadata.doc_type
|
||||
document.metadata = knowledge_config.metadata.doc_metadata
|
||||
db.session.add(document)
|
||||
documents.append(document)
|
||||
duplicate_document_ids.append(document.id)
|
||||
|
|
@ -931,6 +935,7 @@ class DocumentService:
|
|||
account,
|
||||
file_name,
|
||||
batch,
|
||||
knowledge_config.metadata,
|
||||
)
|
||||
db.session.add(document)
|
||||
db.session.flush()
|
||||
|
|
@ -986,6 +991,7 @@ class DocumentService:
|
|||
account,
|
||||
page.page_name,
|
||||
batch,
|
||||
knowledge_config.metadata,
|
||||
)
|
||||
db.session.add(document)
|
||||
db.session.flush()
|
||||
|
|
@ -1026,6 +1032,7 @@ class DocumentService:
|
|||
account,
|
||||
document_name,
|
||||
batch,
|
||||
knowledge_config.metadata,
|
||||
)
|
||||
db.session.add(document)
|
||||
db.session.flush()
|
||||
|
|
@ -1063,6 +1070,7 @@ class DocumentService:
|
|||
account: Account,
|
||||
name: str,
|
||||
batch: str,
|
||||
metadata: Optional[MetaDataConfig] = None,
|
||||
):
|
||||
document = Document(
|
||||
tenant_id=dataset.tenant_id,
|
||||
|
|
@ -1078,6 +1086,9 @@ class DocumentService:
|
|||
doc_form=document_form,
|
||||
doc_language=document_language,
|
||||
)
|
||||
if metadata is not None:
|
||||
document.doc_metadata = metadata.doc_metadata
|
||||
document.doc_type = metadata.doc_type
|
||||
return document
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -1190,6 +1201,10 @@ class DocumentService:
|
|||
# update document name
|
||||
if document_data.name:
|
||||
document.name = document_data.name
|
||||
# update doc_type and doc_metadata if provided
|
||||
if document_data.metadata is not None:
|
||||
document.doc_metadata = document_data.metadata.doc_type
|
||||
document.doc_type = document_data.metadata.doc_type
|
||||
# update document to be waiting
|
||||
document.indexing_status = "waiting"
|
||||
document.completed_at = None
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ class RetrievalModel(BaseModel):
|
|||
score_threshold: Optional[float] = None
|
||||
|
||||
|
||||
class MetaDataConfig(BaseModel):
|
||||
doc_type: str
|
||||
doc_metadata: dict
|
||||
|
||||
|
||||
class KnowledgeConfig(BaseModel):
|
||||
original_document_id: Optional[str] = None
|
||||
duplicate: bool = True
|
||||
|
|
@ -105,6 +110,7 @@ class KnowledgeConfig(BaseModel):
|
|||
embedding_model: Optional[str] = None
|
||||
embedding_model_provider: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
metadata: Optional[MetaDataConfig] = None
|
||||
|
||||
|
||||
class SegmentUpdateArgs(BaseModel):
|
||||
|
|
|
|||
|
|
@ -38,30 +38,22 @@ class WebsiteService:
|
|||
only_main_content = options.get("only_main_content", False)
|
||||
if not crawl_sub_pages:
|
||||
params = {
|
||||
"crawlerOptions": {
|
||||
"includes": [],
|
||||
"excludes": [],
|
||||
"generateImgAltText": True,
|
||||
"limit": 1,
|
||||
"returnOnlyUrls": False,
|
||||
"pageOptions": {"onlyMainContent": only_main_content, "includeHtml": False},
|
||||
}
|
||||
"includePaths": [],
|
||||
"excludePaths": [],
|
||||
"limit": 1,
|
||||
"scrapeOptions": {"onlyMainContent": only_main_content},
|
||||
}
|
||||
else:
|
||||
includes = options.get("includes").split(",") if options.get("includes") else []
|
||||
excludes = options.get("excludes").split(",") if options.get("excludes") else []
|
||||
params = {
|
||||
"crawlerOptions": {
|
||||
"includes": includes,
|
||||
"excludes": excludes,
|
||||
"generateImgAltText": True,
|
||||
"limit": options.get("limit", 1),
|
||||
"returnOnlyUrls": False,
|
||||
"pageOptions": {"onlyMainContent": only_main_content, "includeHtml": False},
|
||||
}
|
||||
"includePaths": includes,
|
||||
"excludePaths": excludes,
|
||||
"limit": options.get("limit", 1),
|
||||
"scrapeOptions": {"onlyMainContent": only_main_content},
|
||||
}
|
||||
if options.get("max_depth"):
|
||||
params["crawlerOptions"]["maxDepth"] = options.get("max_depth")
|
||||
params["maxDepth"] = options.get("max_depth")
|
||||
job_id = firecrawl_app.crawl_url(url, params)
|
||||
website_crawl_time_cache_key = f"website_crawl_{job_id}"
|
||||
time = str(datetime.datetime.now().timestamp())
|
||||
|
|
@ -228,7 +220,7 @@ class WebsiteService:
|
|||
# decrypt api_key
|
||||
api_key = encrypter.decrypt_token(tenant_id=tenant_id, token=credentials.get("config").get("api_key"))
|
||||
firecrawl_app = FirecrawlApp(api_key=api_key, base_url=credentials.get("config").get("base_url", None))
|
||||
params = {"pageOptions": {"onlyMainContent": only_main_content, "includeHtml": False}}
|
||||
params = {"onlyMainContent": only_main_content}
|
||||
result = firecrawl_app.scrape_url(url, params)
|
||||
return result
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -10,19 +10,17 @@ def test_firecrawl_web_extractor_crawl_mode(mocker):
|
|||
base_url = "https://api.firecrawl.dev"
|
||||
firecrawl_app = FirecrawlApp(api_key=api_key, base_url=base_url)
|
||||
params = {
|
||||
"crawlerOptions": {
|
||||
"includes": [],
|
||||
"excludes": [],
|
||||
"generateImgAltText": True,
|
||||
"maxDepth": 1,
|
||||
"limit": 1,
|
||||
"returnOnlyUrls": False,
|
||||
}
|
||||
"includePaths": [],
|
||||
"excludePaths": [],
|
||||
"maxDepth": 1,
|
||||
"limit": 1,
|
||||
}
|
||||
mocked_firecrawl = {
|
||||
"jobId": "test",
|
||||
"id": "test",
|
||||
}
|
||||
mocker.patch("requests.post", return_value=_mock_response(mocked_firecrawl))
|
||||
job_id = firecrawl_app.crawl_url(url, params)
|
||||
print(job_id)
|
||||
print(f"job_id: {job_id}")
|
||||
|
||||
assert job_id is not None
|
||||
assert isinstance(job_id, str)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
|
|||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:1.0.0-beta.1
|
||||
image: langgenius/dify-api:1.0.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
|
|
@ -27,7 +27,7 @@ services:
|
|||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:1.0.0-beta.1
|
||||
image: langgenius/dify-api:1.0.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
|
|
@ -51,7 +51,7 @@ services:
|
|||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:1.0.0-beta.1
|
||||
image: langgenius/dify-web:1.0.0
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@ x-shared-env: &shared-api-worker-env
|
|||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:1.0.0-beta.1
|
||||
image: langgenius/dify-api:1.0.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
|
|
@ -435,7 +435,7 @@ services:
|
|||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:1.0.0-beta.1
|
||||
image: langgenius/dify-api:1.0.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
|
|
@ -459,7 +459,7 @@ services:
|
|||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:1.0.0-beta.1
|
||||
image: langgenius/dify-web:1.0.0
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
|
|||
acl localnet src fc00::/7 # RFC 4193 local private network range
|
||||
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||
acl SSL_ports port 443
|
||||
# acl SSL_ports port 1025-65535 # Enable the configuration to resolve this issue: https://github.com/langgenius/dify/issues/12792
|
||||
acl Safe_ports port 80 # http
|
||||
acl Safe_ports port 21 # ftp
|
||||
acl Safe_ports port 443 # https
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class TestKnowledgeBaseClient(unittest.TestCase):
|
|||
def _test_005_batch_indexing_status(self):
|
||||
client = self._get_dataset_kb_client()
|
||||
response = client.batch_indexing_status(self.batch_id)
|
||||
data = response.json()
|
||||
response.json()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def _test_006_update_document_by_file(self):
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ RUN pnpm add -g pm2 \
|
|||
&& chown -R 1001:0 /.pm2 /app/web \
|
||||
&& chmod -R g=u /.pm2 /app/web
|
||||
|
||||
|
||||
ARG COMMIT_SHA
|
||||
ENV COMMIT_SHA=${COMMIT_SHA}
|
||||
|
||||
|
|
|
|||
|
|
@ -162,9 +162,9 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={cn(s.app, 'flex', 'overflow-hidden')}>
|
||||
<div className={cn(s.app, 'flex relative', 'overflow-hidden')}>
|
||||
{appDetail && (
|
||||
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background} desc={appDetail.mode} navigation={navigation} />
|
||||
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} />
|
||||
)}
|
||||
<div className="bg-components-panel-bg grow overflow-hidden">
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ import AppContext from '@/context/app-context'
|
|||
|
||||
export type ICardViewProps = {
|
||||
appId: string
|
||||
isInPanel?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CardView: FC<ICardViewProps> = ({ appId }) => {
|
||||
const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
|
|
@ -120,10 +122,11 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
|
|||
return <Loading />
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6">
|
||||
<div className={className || 'grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'}>
|
||||
<AppCard
|
||||
appInfo={appDetail}
|
||||
cardType="webapp"
|
||||
isInPanel={isInPanel}
|
||||
onChangeStatus={onChangeSiteStatus}
|
||||
onGenerateCode={onGenerateCode}
|
||||
onSaveSiteConfig={onSaveSiteConfig}
|
||||
|
|
@ -131,6 +134,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
|
|||
<AppCard
|
||||
cardType="api"
|
||||
appInfo={appDetail}
|
||||
isInPanel={isInPanel}
|
||||
onChangeStatus={onChangeApiStatus}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type { LangFuseConfig, LangSmithConfig, OpikConfig } from './type'
|
|||
import { TracingProvider } from './type'
|
||||
import TracingIcon from './tracing-icon'
|
||||
import ConfigButton from './config-button'
|
||||
import cn from '@/utils/classnames'
|
||||
import { LangfuseIcon, LangsmithIcon, OpikIcon } from '@/app/components/base/icons/src/public/tracing'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
|
||||
|
|
@ -19,7 +20,6 @@ import Toast from '@/app/components/base/toast'
|
|||
import { useAppContext } from '@/context/app-context'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const I18N_PREFIX = 'app.tracing'
|
||||
|
||||
|
|
@ -80,7 +80,9 @@ const Panel: FC = () => {
|
|||
? LangsmithIcon
|
||||
: inUseTracingProvider === TracingProvider.langfuse
|
||||
? LangfuseIcon
|
||||
: OpikIcon
|
||||
: inUseTracingProvider === TracingProvider.opik
|
||||
? OpikIcon
|
||||
: null
|
||||
|
||||
const [langSmithConfig, setLangSmithConfig] = useState<LangSmithConfig | null>(null)
|
||||
const [langFuseConfig, setLangFuseConfig] = useState<LangFuseConfig | null>(null)
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ const ProviderConfigModal: FC<Props> = ({
|
|||
>
|
||||
<span className='text-[#D92D20]'>{t('common.operation.remove')}</span>
|
||||
</Button>
|
||||
<Divider className='mx-3 h-[18px]'/>
|
||||
<Divider className='mx-3 h-[18px]' />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -30,9 +30,7 @@ const ApiServer: FC<ApiServerProps> = ({
|
|||
{t('appApi.ok')}
|
||||
</div>
|
||||
<SecretKeyButton
|
||||
className='flex-shrink-0 !h-8 bg-white'
|
||||
textCls='!text-gray-700 font-medium'
|
||||
iconCls='stroke-[1.2px]'
|
||||
className='shrink-0 !h-8 bg-white'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,44 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||
<Property name='text' type='string' key='text'>
|
||||
Document content
|
||||
</Property>
|
||||
<Property name='doc_type' type='string' key='doc_type'>
|
||||
Type of document (optional):
|
||||
- <code>book</code> Book
|
||||
- <code>web_page</code> Web page
|
||||
- <code>paper</code> Academic paper/article
|
||||
- <code>social_media_post</code> Social media post
|
||||
- <code>wikipedia_entry</code> Wikipedia entry
|
||||
- <code>personal_document</code> Personal document
|
||||
- <code>business_document</code> Business document
|
||||
- <code>im_chat_log</code> Chat log
|
||||
- <code>synced_from_notion</code> Notion document
|
||||
- <code>synced_from_github</code> GitHub document
|
||||
- <code>others</code> Other document types
|
||||
</Property>
|
||||
<Property name='doc_metadata' type='object' key='doc_metadata'>
|
||||
Document metadata (required if doc_type is provided). Fields vary by doc_type:
|
||||
For <code>book</code>:
|
||||
- <code>title</code> Book title
|
||||
- <code>language</code> Book language
|
||||
- <code>author</code> Book author
|
||||
- <code>publisher</code> Publisher name
|
||||
- <code>publication_date</code> Publication date
|
||||
- <code>isbn</code> ISBN number
|
||||
- <code>category</code> Book category
|
||||
|
||||
For <code>web_page</code>:
|
||||
- <code>title</code> Page title
|
||||
- <code>url</code> Page URL
|
||||
- <code>language</code> Page language
|
||||
- <code>publish_date</code> Publish date
|
||||
- <code>author/publisher</code> Author or publisher
|
||||
- <code>topic/keywords</code> Topic or keywords
|
||||
- <code>description</code> Page description
|
||||
|
||||
Please check [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) for more details on the fields required for each doc_type.
|
||||
|
||||
For doc_type "others", any valid JSON object is accepted
|
||||
</Property>
|
||||
<Property name='indexing_technique' type='string' key='indexing_technique'>
|
||||
Index mode
|
||||
- <code>high_quality</code> High quality: embedding using embedding model, built as vector database index
|
||||
|
|
@ -195,6 +233,68 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||
- <code>hierarchical_model</code> Parent-child mode
|
||||
- <code>qa_model</code> Q&A Mode: Generates Q&A pairs for segmented documents and then embeds the questions
|
||||
|
||||
- <code>doc_type</code> Type of document (optional)
|
||||
- <code>book</code> Book
|
||||
Document records a book or publication
|
||||
- <code>web_page</code> Web page
|
||||
Document records web page content
|
||||
- <code>paper</code> Academic paper/article
|
||||
Document records academic paper or research article
|
||||
- <code>social_media_post</code> Social media post
|
||||
Content from social media posts
|
||||
- <code>wikipedia_entry</code> Wikipedia entry
|
||||
Content from Wikipedia entries
|
||||
- <code>personal_document</code> Personal document
|
||||
Documents related to personal content
|
||||
- <code>business_document</code> Business document
|
||||
Documents related to business content
|
||||
- <code>im_chat_log</code> Chat log
|
||||
Records of instant messaging chats
|
||||
- <code>synced_from_notion</code> Notion document
|
||||
Documents synchronized from Notion
|
||||
- <code>synced_from_github</code> GitHub document
|
||||
Documents synchronized from GitHub
|
||||
- <code>others</code> Other document types
|
||||
Other document types not listed above
|
||||
|
||||
- <code>doc_metadata</code> Document metadata (required if doc_type is provided)
|
||||
Fields vary by doc_type:
|
||||
|
||||
For <code>book</code>:
|
||||
- <code>title</code> Book title
|
||||
Title of the book
|
||||
- <code>language</code> Book language
|
||||
Language of the book
|
||||
- <code>author</code> Book author
|
||||
Author of the book
|
||||
- <code>publisher</code> Publisher name
|
||||
Name of the publishing house
|
||||
- <code>publication_date</code> Publication date
|
||||
Date when the book was published
|
||||
- <code>isbn</code> ISBN number
|
||||
International Standard Book Number
|
||||
- <code>category</code> Book category
|
||||
Category or genre of the book
|
||||
|
||||
For <code>web_page</code>:
|
||||
- <code>title</code> Page title
|
||||
Title of the web page
|
||||
- <code>url</code> Page URL
|
||||
URL address of the web page
|
||||
- <code>language</code> Page language
|
||||
Language of the web page
|
||||
- <code>publish_date</code> Publish date
|
||||
Date when the web page was published
|
||||
- <code>author/publisher</code> Author or publisher
|
||||
Author or publisher of the web page
|
||||
- <code>topic/keywords</code> Topic or keywords
|
||||
Topics or keywords of the web page
|
||||
- <code>description</code> Page description
|
||||
Description of the web page content
|
||||
|
||||
Please check [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) for more details on the fields required for each doc_type.
|
||||
For doc_type "others", any valid JSON object is accepted
|
||||
|
||||
- <code>doc_language</code> In Q&A mode, specify the language of the document, for example: <code>English</code>, <code>Chinese</code>
|
||||
|
||||
- <code>process_rule</code> Processing rules
|
||||
|
|
@ -307,6 +407,44 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||
<Property name='description' type='string' key='description'>
|
||||
Knowledge description (optional)
|
||||
</Property>
|
||||
<Property name='doc_type' type='string' key='doc_type'>
|
||||
Type of document (optional):
|
||||
- <code>book</code> Book
|
||||
- <code>web_page</code> Web page
|
||||
- <code>paper</code> Academic paper/article
|
||||
- <code>social_media_post</code> Social media post
|
||||
- <code>wikipedia_entry</code> Wikipedia entry
|
||||
- <code>personal_document</code> Personal document
|
||||
- <code>business_document</code> Business document
|
||||
- <code>im_chat_log</code> Chat log
|
||||
- <code>synced_from_notion</code> Notion document
|
||||
- <code>synced_from_github</code> GitHub document
|
||||
- <code>others</code> Other document types
|
||||
</Property>
|
||||
<Property name='doc_metadata' type='object' key='doc_metadata'>
|
||||
Document metadata (required if doc_type is provided). Fields vary by doc_type:
|
||||
For <code>book</code>:
|
||||
- <code>title</code> Book title
|
||||
- <code>language</code> Book language
|
||||
- <code>author</code> Book author
|
||||
- <code>publisher</code> Publisher name
|
||||
- <code>publication_date</code> Publication date
|
||||
- <code>isbn</code> ISBN number
|
||||
- <code>category</code> Book category
|
||||
|
||||
For <code>web_page</code>:
|
||||
- <code>title</code> Page title
|
||||
- <code>url</code> Page URL
|
||||
- <code>language</code> Page language
|
||||
- <code>publish_date</code> Publish date
|
||||
- <code>author/publisher</code> Author or publisher
|
||||
- <code>topic/keywords</code> Topic or keywords
|
||||
- <code>description</code> Page description
|
||||
|
||||
Please check [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) for more details on the fields required for each doc_type.
|
||||
|
||||
For doc_type "others", any valid JSON object is accepted
|
||||
</Property>
|
||||
<Property name='indexing_technique' type='string' key='indexing_technique'>
|
||||
Index technique (optional)
|
||||
- <code>high_quality</code> High quality
|
||||
|
|
@ -624,6 +762,67 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||
- <code>separator</code> Segmentation identifier. Currently, only one delimiter is allowed. The default is <code>***</code>
|
||||
- <code>max_tokens</code> The maximum length (tokens) must be validated to be shorter than the length of the parent chunk
|
||||
- <code>chunk_overlap</code> Define the overlap between adjacent chunks (optional)
|
||||
- <code>doc_type</code> Type of document (optional)
|
||||
- <code>book</code> Book
|
||||
Document records a book or publication
|
||||
- <code>web_page</code> Web page
|
||||
Document records web page content
|
||||
- <code>paper</code> Academic paper/article
|
||||
Document records academic paper or research article
|
||||
- <code>social_media_post</code> Social media post
|
||||
Content from social media posts
|
||||
- <code>wikipedia_entry</code> Wikipedia entry
|
||||
Content from Wikipedia entries
|
||||
- <code>personal_document</code> Personal document
|
||||
Documents related to personal content
|
||||
- <code>business_document</code> Business document
|
||||
Documents related to business content
|
||||
- <code>im_chat_log</code> Chat log
|
||||
Records of instant messaging chats
|
||||
- <code>synced_from_notion</code> Notion document
|
||||
Documents synchronized from Notion
|
||||
- <code>synced_from_github</code> GitHub document
|
||||
Documents synchronized from GitHub
|
||||
- <code>others</code> Other document types
|
||||
Other document types not listed above
|
||||
|
||||
- <code>doc_metadata</code> Document metadata (required if doc_type is provided)
|
||||
Fields vary by doc_type:
|
||||
|
||||
For <code>book</code>:
|
||||
- <code>title</code> Book title
|
||||
Title of the book
|
||||
- <code>language</code> Book language
|
||||
Language of the book
|
||||
- <code>author</code> Book author
|
||||
Author of the book
|
||||
- <code>publisher</code> Publisher name
|
||||
Name of the publishing house
|
||||
- <code>publication_date</code> Publication date
|
||||
Date when the book was published
|
||||
- <code>isbn</code> ISBN number
|
||||
International Standard Book Number
|
||||
- <code>category</code> Book category
|
||||
Category or genre of the book
|
||||
|
||||
For <code>web_page</code>:
|
||||
- <code>title</code> Page title
|
||||
Title of the web page
|
||||
- <code>url</code> Page URL
|
||||
URL address of the web page
|
||||
- <code>language</code> Page language
|
||||
Language of the web page
|
||||
- <code>publish_date</code> Publish date
|
||||
Date when the web page was published
|
||||
- <code>author/publisher</code> Author or publisher
|
||||
Author or publisher of the web page
|
||||
- <code>topic/keywords</code> Topic or keywords
|
||||
Topics or keywords of the web page
|
||||
- <code>description</code> Page description
|
||||
Description of the web page content
|
||||
|
||||
Please check [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) for more details on the fields required for each doc_type.
|
||||
For doc_type "others", any valid JSON object is accepted
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,46 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||
<Property name='text' type='string' key='text'>
|
||||
文档内容
|
||||
</Property>
|
||||
<Property name='doc_type' type='string' key='doc_type'>
|
||||
文档类型(选填)
|
||||
- <code>book</code> 图书 Book
|
||||
- <code>web_page</code> 网页 Web page
|
||||
- <code>paper</code> 学术论文/文章 Academic paper/article
|
||||
- <code>social_media_post</code> 社交媒体帖子 Social media post
|
||||
- <code>wikipedia_entry</code> 维基百科条目 Wikipedia entry
|
||||
- <code>personal_document</code> 个人文档 Personal document
|
||||
- <code>business_document</code> 商业文档 Business document
|
||||
- <code>im_chat_log</code> 即时通讯记录 Chat log
|
||||
- <code>synced_from_notion</code> Notion同步文档 Notion document
|
||||
- <code>synced_from_github</code> GitHub同步文档 GitHub document
|
||||
- <code>others</code> 其他文档类型 Other document types
|
||||
</Property>
|
||||
<Property name='doc_metadata' type='object' key='doc_metadata'>
|
||||
|
||||
文档元数据(如提供文档类型则必填)。字段因文档类型而异:
|
||||
|
||||
针对图书 For <code>book</code>:
|
||||
- <code>title</code> 书名 Book title
|
||||
- <code>language</code> 图书语言 Book language
|
||||
- <code>author</code> 作者 Book author
|
||||
- <code>publisher</code> 出版社 Publisher name
|
||||
- <code>publication_date</code> 出版日期 Publication date
|
||||
- <code>isbn</code> ISBN号码 ISBN number
|
||||
- <code>category</code> 图书分类 Book category
|
||||
|
||||
针对网页 For <code>web_page</code>:
|
||||
- <code>title</code> 页面标题 Page title
|
||||
- <code>url</code> 页面网址 Page URL
|
||||
- <code>language</code> 页面语言 Page language
|
||||
- <code>publish_date</code> 发布日期 Publish date
|
||||
- <code>author/publisher</code> 作者/发布者 Author or publisher
|
||||
- <code>topic/keywords</code> 主题/关键词 Topic or keywords
|
||||
- <code>description</code> 页面描述 Page description
|
||||
|
||||
请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
|
||||
|
||||
针对"其他"类型文档,接受任何有效的JSON对象
|
||||
</Property>
|
||||
<Property name='indexing_technique' type='string' key='indexing_technique'>
|
||||
索引方式
|
||||
- <code>high_quality</code> 高质量:使用 embedding 模型进行嵌入,构建为向量数据库索引
|
||||
|
|
@ -194,6 +234,68 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||
- <code>text_model</code> text 文档直接 embedding,经济模式默认为该模式
|
||||
- <code>hierarchical_model</code> parent-child 模式
|
||||
- <code>qa_model</code> Q&A 模式:为分片文档生成 Q&A 对,然后对问题进行 embedding
|
||||
- <code>doc_type</code> 文档类型(选填)Type of document (optional)
|
||||
- <code>book</code> 图书
|
||||
文档记录一本书籍或出版物
|
||||
- <code>web_page</code> 网页
|
||||
网页内容的文档记录
|
||||
- <code>paper</code> 学术论文/文章
|
||||
学术论文或研究文章的记录
|
||||
- <code>social_media_post</code> 社交媒体帖子
|
||||
社交媒体上的帖子内容
|
||||
- <code>wikipedia_entry</code> 维基百科条目
|
||||
维基百科的词条内容
|
||||
- <code>personal_document</code> 个人文档
|
||||
个人相关的文档记录
|
||||
- <code>business_document</code> 商业文档
|
||||
商业相关的文档记录
|
||||
- <code>im_chat_log</code> 即时通讯记录
|
||||
即时通讯的聊天记录
|
||||
- <code>synced_from_notion</code> Notion同步文档
|
||||
从Notion同步的文档内容
|
||||
- <code>synced_from_github</code> GitHub同步文档
|
||||
从GitHub同步的文档内容
|
||||
- <code>others</code> 其他文档类型
|
||||
其他未列出的文档类型
|
||||
|
||||
- <code>doc_metadata</code> 文档元数据(如提供文档类型则必填
|
||||
字段因文档类型而异
|
||||
|
||||
针对图书类型 For <code>book</code>:
|
||||
- <code>title</code> 书名
|
||||
书籍的标题
|
||||
- <code>language</code> 图书语言
|
||||
书籍的语言
|
||||
- <code>author</code> 作者
|
||||
书籍的作者
|
||||
- <code>publisher</code> 出版社
|
||||
出版社的名称
|
||||
- <code>publication_date</code> 出版日期
|
||||
书籍的出版日期
|
||||
- <code>isbn</code> ISBN号码
|
||||
书籍的ISBN编号
|
||||
- <code>category</code> 图书分类
|
||||
书籍的分类类别
|
||||
|
||||
针对网页类型 For <code>web_page</code>:
|
||||
- <code>title</code> 页面标题
|
||||
网页的标题
|
||||
- <code>url</code> 页面网址
|
||||
网页的URL地址
|
||||
- <code>language</code> 页面语言
|
||||
网页的语言
|
||||
- <code>publish_date</code> 发布日期
|
||||
网页的发布日期
|
||||
- <code>author/publisher</code> 作者/发布者
|
||||
网页的作者或发布者
|
||||
- <code>topic/keywords</code> 主题/关键词
|
||||
网页的主题或关键词
|
||||
- <code>description</code> 页面描述
|
||||
网页的描述信息
|
||||
|
||||
请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
|
||||
|
||||
针对"其他"类型文档,接受任何有效的JSON对象
|
||||
|
||||
- <code>doc_language</code> 在 Q&A 模式下,指定文档的语言,例如:<code>English</code>、<code>Chinese</code>
|
||||
|
||||
|
|
@ -504,6 +606,46 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||
<Property name='text' type='string' key='text'>
|
||||
文档内容(选填)
|
||||
</Property>
|
||||
<Property name='doc_type' type='string' key='doc_type'>
|
||||
文档类型(选填)
|
||||
- <code>book</code> 图书 Book
|
||||
- <code>web_page</code> 网页 Web page
|
||||
- <code>paper</code> 学术论文/文章 Academic paper/article
|
||||
- <code>social_media_post</code> 社交媒体帖子 Social media post
|
||||
- <code>wikipedia_entry</code> 维基百科条目 Wikipedia entry
|
||||
- <code>personal_document</code> 个人文档 Personal document
|
||||
- <code>business_document</code> 商业文档 Business document
|
||||
- <code>im_chat_log</code> 即时通讯记录 Chat log
|
||||
- <code>synced_from_notion</code> Notion同步文档 Notion document
|
||||
- <code>synced_from_github</code> GitHub同步文档 GitHub document
|
||||
- <code>others</code> 其他文档类型 Other document types
|
||||
</Property>
|
||||
<Property name='doc_metadata' type='object' key='doc_metadata'>
|
||||
|
||||
文档元数据(如提供文档类型则必填)。字段因文档类型而异:
|
||||
|
||||
针对图书 For <code>book</code>:
|
||||
- <code>title</code> 书名 Book title
|
||||
- <code>language</code> 图书语言 Book language
|
||||
- <code>author</code> 作者 Book author
|
||||
- <code>publisher</code> 出版社 Publisher name
|
||||
- <code>publication_date</code> 出版日期 Publication date
|
||||
- <code>isbn</code> ISBN号码 ISBN number
|
||||
- <code>category</code> 图书分类 Book category
|
||||
|
||||
针对网页 For <code>web_page</code>:
|
||||
- <code>title</code> 页面标题 Page title
|
||||
- <code>url</code> 页面网址 Page URL
|
||||
- <code>language</code> 页面语言 Page language
|
||||
- <code>publish_date</code> 发布日期 Publish date
|
||||
- <code>author/publisher</code> 作者/发布者 Author or publisher
|
||||
- <code>topic/keywords</code> 主题/关键词 Topic or keywords
|
||||
- <code>description</code> 页面描述 Page description
|
||||
|
||||
请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
|
||||
|
||||
针对"其他"类型文档,接受任何有效的JSON对象
|
||||
</Property>
|
||||
<Property name='process_rule' type='object' key='process_rule'>
|
||||
处理规则(选填)
|
||||
- <code>mode</code> (string) 清洗、分段模式 ,automatic 自动 / custom 自定义
|
||||
|
|
@ -624,6 +766,68 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||
- <code>separator</code> 分段标识符,目前仅允许设置一个分隔符。默认为 <code>***</code>
|
||||
- <code>max_tokens</code> 最大长度 (token) 需要校验小于父级的长度
|
||||
- <code>chunk_overlap</code> 分段重叠指的是在对数据进行分段时,段与段之间存在一定的重叠部分(选填)
|
||||
- <code>doc_type</code> 文档类型(选填)Type of document (optional)
|
||||
- <code>book</code> 图书
|
||||
文档记录一本书籍或出版物
|
||||
- <code>web_page</code> 网页
|
||||
网页内容的文档记录
|
||||
- <code>paper</code> 学术论文/文章
|
||||
学术论文或研究文章的记录
|
||||
- <code>social_media_post</code> 社交媒体帖子
|
||||
社交媒体上的帖子内容
|
||||
- <code>wikipedia_entry</code> 维基百科条目
|
||||
维基百科的词条内容
|
||||
- <code>personal_document</code> 个人文档
|
||||
个人相关的文档记录
|
||||
- <code>business_document</code> 商业文档
|
||||
商业相关的文档记录
|
||||
- <code>im_chat_log</code> 即时通讯记录
|
||||
即时通讯的聊天记录
|
||||
- <code>synced_from_notion</code> Notion同步文档
|
||||
从Notion同步的文档内容
|
||||
- <code>synced_from_github</code> GitHub同步文档
|
||||
从GitHub同步的文档内容
|
||||
- <code>others</code> 其他文档类型
|
||||
其他未列出的文档类型
|
||||
|
||||
- <code>doc_metadata</code> 文档元数据(如提供文档类型则必填
|
||||
字段因文档类型而异
|
||||
|
||||
针对图书类型 For <code>book</code>:
|
||||
- <code>title</code> 书名
|
||||
书籍的标题
|
||||
- <code>language</code> 图书语言
|
||||
书籍的语言
|
||||
- <code>author</code> 作者
|
||||
书籍的作者
|
||||
- <code>publisher</code> 出版社
|
||||
出版社的名称
|
||||
- <code>publication_date</code> 出版日期
|
||||
书籍的出版日期
|
||||
- <code>isbn</code> ISBN号码
|
||||
书籍的ISBN编号
|
||||
- <code>category</code> 图书分类
|
||||
书籍的分类类别
|
||||
|
||||
针对网页类型 For <code>web_page</code>:
|
||||
- <code>title</code> 页面标题
|
||||
网页的标题
|
||||
- <code>url</code> 页面网址
|
||||
网页的URL地址
|
||||
- <code>language</code> 页面语言
|
||||
网页的语言
|
||||
- <code>publish_date</code> 发布日期
|
||||
网页的发布日期
|
||||
- <code>author/publisher</code> 作者/发布者
|
||||
网页的作者或发布者
|
||||
- <code>topic/keywords</code> 主题/关键词
|
||||
网页的主题或关键词
|
||||
- <code>description</code> 页面描述
|
||||
网页的描述信息
|
||||
|
||||
请查看 [api/services/dataset_service.py](https://github.com/langgenius/dify/blob/main/api/services/dataset_service.py#L475) 了解各文档类型所需字段的详细信息。
|
||||
|
||||
针对"其他"类型文档,接受任何有效的JSON对象
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
RiEqualizer2Line,
|
||||
RiFileCopy2Line,
|
||||
RiFileDownloadLine,
|
||||
RiFileUploadLine,
|
||||
} from '@remixicon/react'
|
||||
import AppIcon from '../base/app-icon'
|
||||
import SwitchAppModal from '../app/switch-app-modal'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
|
|
@ -22,8 +22,6 @@ import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/ap
|
|||
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
|
||||
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
|
|
@ -31,6 +29,9 @@ import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal'
|
|||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import ContentDialog from '@/app/components/base/content-dialog'
|
||||
import Button from '@/app/components/base/button'
|
||||
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView'
|
||||
|
||||
export type IAppInfoProps = {
|
||||
expand: boolean
|
||||
|
|
@ -47,7 +48,6 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
|||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [showSwitchTip, setShowSwitchTip] = useState<string>('')
|
||||
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
|
||||
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
|
|
@ -183,291 +183,199 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
|||
return null
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => {
|
||||
if (isCurrentWorkspaceEditor)
|
||||
setOpen(v => !v)
|
||||
}}
|
||||
className='block'
|
||||
>
|
||||
<div className={cn('flex p-1 rounded-lg', open && 'bg-gray-100', isCurrentWorkspaceEditor && 'hover:bg-gray-100 cursor-pointer')}>
|
||||
<div className='relative shrink-0 mr-2'>
|
||||
<AppIcon
|
||||
size={expand ? 'large' : 'small'}
|
||||
iconType={appDetail.icon_type}
|
||||
icon={appDetail.icon}
|
||||
background={appDetail.icon_background}
|
||||
imageUrl={appDetail.icon_url}
|
||||
/>
|
||||
<span className={cn(
|
||||
'absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm',
|
||||
!expand && '!w-3.5 !h-3.5 !bottom-[-2px] !right-[-2px]',
|
||||
)}>
|
||||
{appDetail.mode === 'advanced-chat' && (
|
||||
<ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
|
||||
)}
|
||||
{appDetail.mode === 'agent-chat' && (
|
||||
<CuteRobot className={cn('w-3 h-3 text-indigo-600', !expand && '!w-2.5 !h-2.5')} />
|
||||
)}
|
||||
{appDetail.mode === 'chat' && (
|
||||
<ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
|
||||
)}
|
||||
{appDetail.mode === 'completion' && (
|
||||
<AiText className={cn('w-3 h-3 text-[#0E9384]', !expand && '!w-2.5 !h-2.5')} />
|
||||
)}
|
||||
{appDetail.mode === 'workflow' && (
|
||||
<Route className={cn('w-3 h-3 text-[#f79009]', !expand && '!w-2.5 !h-2.5')} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{expand && (
|
||||
<div className="grow w-0">
|
||||
<div className='flex justify-between items-center text-sm leading-5 font-medium text-text-secondary'>
|
||||
<div className='truncate' title={appDetail.name}>{appDetail.name}</div>
|
||||
{isCurrentWorkspaceEditor && <RiArrowDownSLine className='shrink-0 ml-[2px] w-3 h-3 text-gray-500' />}
|
||||
</div>
|
||||
<div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
|
||||
{appDetail.mode === 'advanced-chat' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||
<div title={t('app.types.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.advanced').toUpperCase()}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'agent-chat' && (
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
|
||||
)}
|
||||
{appDetail.mode === 'chat' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'completion' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
|
||||
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'workflow' && (
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (isCurrentWorkspaceEditor)
|
||||
setOpen(v => !v)
|
||||
}}
|
||||
className='block w-full'
|
||||
>
|
||||
<div className={cn('flex rounded-lg', expand ? 'p-2 pb-2.5 flex-col gap-2' : 'p-1 gap-1 justify-center items-start', open && 'bg-state-base-hover', isCurrentWorkspaceEditor && 'hover:bg-state-base-hover cursor-pointer')}>
|
||||
<div className={`flex items-center self-stretch ${expand ? 'justify-between' : 'flex-col gap-1'}`}>
|
||||
<AppIcon
|
||||
size={expand ? 'large' : 'small'}
|
||||
iconType={appDetail.icon_type}
|
||||
icon={appDetail.icon}
|
||||
background={appDetail.icon_background}
|
||||
imageUrl={appDetail.icon_url}
|
||||
/>
|
||||
<div className='flex p-0.5 justify-center items-center rounded-md'>
|
||||
<div className='flex w-5 h-5 justify-center items-center'>
|
||||
<RiEqualizer2Line className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1002]'>
|
||||
<div className='relative w-[320px] bg-white rounded-2xl shadow-xl'>
|
||||
{/* header */}
|
||||
<div className={cn('flex pl-4 pt-3 pr-3', !appDetail.description && 'pb-2')}>
|
||||
<div className='relative shrink-0 mr-2'>
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={appDetail.icon_type}
|
||||
icon={appDetail.icon}
|
||||
background={appDetail.icon_background}
|
||||
imageUrl={appDetail.icon_url}
|
||||
/>
|
||||
<span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
|
||||
{appDetail.mode === 'advanced-chat' && (
|
||||
<ChatBot className='w-3 h-3 text-[#1570EF]' />
|
||||
)}
|
||||
{appDetail.mode === 'agent-chat' && (
|
||||
<CuteRobot className='w-3 h-3 text-indigo-600' />
|
||||
)}
|
||||
{appDetail.mode === 'chat' && (
|
||||
<ChatBot className='w-3 h-3 text-[#1570EF]' />
|
||||
)}
|
||||
{appDetail.mode === 'completion' && (
|
||||
<AiText className='w-3 h-3 text-[#0E9384]' />
|
||||
)}
|
||||
{appDetail.mode === 'workflow' && (
|
||||
<Route className='w-3 h-3 text-[#f79009]' />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className='grow w-0'>
|
||||
<div title={appDetail.name} className='flex justify-between items-center text-sm leading-5 font-medium text-gray-900 truncate'>{appDetail.name}</div>
|
||||
<div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
|
||||
{appDetail.mode === 'advanced-chat' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||
<div title={t('app.types.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.advanced').toUpperCase()}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'agent-chat' && (
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
|
||||
)}
|
||||
{appDetail.mode === 'chat' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'completion' && (
|
||||
<>
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
|
||||
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||
</>
|
||||
)}
|
||||
{appDetail.mode === 'workflow' && (
|
||||
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
|
||||
)}
|
||||
{
|
||||
expand && (
|
||||
<div className='flex flex-col items-start gap-1'>
|
||||
<div className='flex w-full'>
|
||||
<div className='text-text-secondary system-md-semibold truncate'>{appDetail.name}</div>
|
||||
</div>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{appDetail.mode === 'advanced-chat' ? t('app.types.chatbot') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
<ContentDialog
|
||||
show={open}
|
||||
onClose={() => setOpen(false)}
|
||||
className='!p-0 flex flex-col absolute left-2 top-2 bottom-2 w-[420px] rounded-2xl'
|
||||
>
|
||||
<div className='flex p-4 flex-col justify-center items-start gap-3 self-stretch shrink-0'>
|
||||
<div className='flex items-center gap-3 self-stretch'>
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={appDetail.icon_type}
|
||||
icon={appDetail.icon}
|
||||
background={appDetail.icon_background}
|
||||
imageUrl={appDetail.icon_url}
|
||||
/>
|
||||
<div className='flex flex-col justify-center items-start grow w-full'>
|
||||
<div className='text-text-secondary system-md-semibold truncate w-full'>{appDetail.name}</div>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{appDetail.mode === 'advanced-chat' ? t('app.types.chatbot') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
|
||||
</div>
|
||||
{/* description */}
|
||||
{appDetail.description && (
|
||||
<div className='px-4 py-2 text-gray-500 text-xs leading-[18px]'>{appDetail.description}</div>
|
||||
)}
|
||||
{/* operations */}
|
||||
<Divider className="!my-1" />
|
||||
<div className="w-full py-1">
|
||||
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
|
||||
</div>
|
||||
{/* description */}
|
||||
{appDetail.description && (
|
||||
<div className='text-text-tertiary system-xs-regular'>{appDetail.description}</div>
|
||||
)}
|
||||
{/* operations */}
|
||||
<div className='flex items-center gap-1 self-stretch'>
|
||||
<Button
|
||||
size={'small'}
|
||||
variant={'secondary'}
|
||||
className='gap-[1px]'
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setShowEditModal(true)
|
||||
}}>
|
||||
<span className='text-gray-700 text-sm leading-5'>{t('app.editApp')}</span>
|
||||
</div>
|
||||
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
|
||||
}}
|
||||
>
|
||||
<RiEditLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
||||
<span className='text-components-button-secondary-text system-xs-medium'>{t('app.editApp')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
variant={'secondary'}
|
||||
className='gap-[1px]'
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setShowDuplicateModal(true)
|
||||
}}>
|
||||
<span className='text-gray-700 text-sm leading-5'>{t('app.duplicate')}</span>
|
||||
</div>
|
||||
{(appDetail.mode === 'completion' || appDetail.mode === 'chat') && (
|
||||
<>
|
||||
<Divider className="!my-1" />
|
||||
<div
|
||||
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
|
||||
onMouseEnter={() => setShowSwitchTip(appDetail.mode)}
|
||||
onMouseLeave={() => setShowSwitchTip('')}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setShowSwitchModal(true)
|
||||
}}
|
||||
>
|
||||
<span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Divider className="!my-1" />
|
||||
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={exportCheck}>
|
||||
<span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span>
|
||||
</div>
|
||||
{
|
||||
(appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (
|
||||
<div
|
||||
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setShowImportDSLModal(true)
|
||||
}}>
|
||||
<span className='text-gray-700 text-sm leading-5'>{t('workflow.common.importDSL')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Divider className="!my-1" />
|
||||
<div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => {
|
||||
setOpen(false)
|
||||
setShowConfirmDelete(true)
|
||||
}}>
|
||||
<span className='text-gray-700 text-sm leading-5 group-hover:text-red-500'>
|
||||
{t('common.operation.delete')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* switch tip */}
|
||||
<div
|
||||
className={cn(
|
||||
'hidden absolute left-[324px] top-0 w-[376px] rounded-xl bg-white border-[0.5px] border-[rgba(0,0,0,0.05)] shadow-lg',
|
||||
showSwitchTip && '!block',
|
||||
)}
|
||||
}}
|
||||
>
|
||||
<div className={cn(
|
||||
'w-full h-[256px] bg-center bg-no-repeat bg-contain rounded-xl',
|
||||
showSwitchTip === 'chat' && s.expertPic,
|
||||
showSwitchTip === 'completion' && s.completionPic,
|
||||
)} />
|
||||
<div className='px-4 pb-2'>
|
||||
<div className='flex items-center gap-1 text-gray-700 text-md leading-6 font-semibold'>
|
||||
{showSwitchTip === 'chat' ? t('app.types.advanced') : t('app.types.workflow')}
|
||||
<span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
|
||||
</div>
|
||||
<div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.advancedFor').toLocaleUpperCase()}</div>
|
||||
<div className='mt-1 text-gray-500 text-sm leading-5'>{t('app.newApp.advancedDescription')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<RiFileCopy2Line className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
||||
<span className='text-components-button-secondary-text system-xs-medium'>{t('app.duplicate')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
variant={'secondary'}
|
||||
className='gap-[1px]'
|
||||
onClick={exportCheck}
|
||||
>
|
||||
<RiFileDownloadLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
||||
<span className='text-components-button-secondary-text system-xs-medium'>{t('app.export')}</span>
|
||||
</Button>
|
||||
{
|
||||
(appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (
|
||||
<Button
|
||||
size={'small'}
|
||||
variant={'secondary'}
|
||||
className='gap-[1px]'
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setShowImportDSLModal(true)
|
||||
}}
|
||||
>
|
||||
<RiFileUploadLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
||||
<span className='text-components-button-secondary-text system-xs-medium'>{t('workflow.common.importDSL')}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
{showSwitchModal && (
|
||||
<SwitchAppModal
|
||||
inAppDetail
|
||||
show={showSwitchModal}
|
||||
appDetail={appDetail}
|
||||
onClose={() => setShowSwitchModal(false)}
|
||||
onSuccess={() => setShowSwitchModal(false)}
|
||||
</div>
|
||||
<div className='flex flex-1'>
|
||||
<CardView
|
||||
appId={appDetail.id}
|
||||
isInPanel={true}
|
||||
className='flex flex-col px-2 py-1 gap-2 grow overflow-auto'
|
||||
/>
|
||||
)}
|
||||
{showEditModal && (
|
||||
<CreateAppModal
|
||||
isEditModal
|
||||
appName={appDetail.name}
|
||||
appIconType={appDetail.icon_type}
|
||||
appIcon={appDetail.icon}
|
||||
appIconBackground={appDetail.icon_background}
|
||||
appIconUrl={appDetail.icon_url}
|
||||
appDescription={appDetail.description}
|
||||
appMode={appDetail.mode}
|
||||
appUseIconAsAnswerIcon={appDetail.use_icon_as_answer_icon}
|
||||
show={showEditModal}
|
||||
onConfirm={onEdit}
|
||||
onHide={() => setShowEditModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showDuplicateModal && (
|
||||
<DuplicateAppModal
|
||||
appName={appDetail.name}
|
||||
icon_type={appDetail.icon_type}
|
||||
icon={appDetail.icon}
|
||||
icon_background={appDetail.icon_background}
|
||||
icon_url={appDetail.icon_url}
|
||||
show={showDuplicateModal}
|
||||
onConfirm={onCopy}
|
||||
onHide={() => setShowDuplicateModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('app.deleteAppConfirmTitle')}
|
||||
content={t('app.deleteAppConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
{showImportDSLModal && (
|
||||
<UpdateDSLModal
|
||||
onCancel={() => setShowImportDSLModal(false)}
|
||||
onBackup={exportCheck}
|
||||
/>
|
||||
)}
|
||||
{secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
onConfirm={onExport}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
</div>
|
||||
<div className='flex p-2 flex-col justify-center items-start gap-3 self-stretch border-t-[0.5px] border-divider-subtle shrink-0 min-h-fit'>
|
||||
<Button
|
||||
size={'medium'}
|
||||
variant={'ghost'}
|
||||
className='gap-0.5'
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setShowConfirmDelete(true)
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary' />
|
||||
<span className='text-text-tertiary system-sm-medium'>{t('common.operation.deleteApp')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</ContentDialog>
|
||||
{showSwitchModal && (
|
||||
<SwitchAppModal
|
||||
inAppDetail
|
||||
show={showSwitchModal}
|
||||
appDetail={appDetail}
|
||||
onClose={() => setShowSwitchModal(false)}
|
||||
onSuccess={() => setShowSwitchModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showEditModal && (
|
||||
<CreateAppModal
|
||||
isEditModal
|
||||
appName={appDetail.name}
|
||||
appIconType={appDetail.icon_type}
|
||||
appIcon={appDetail.icon}
|
||||
appIconBackground={appDetail.icon_background}
|
||||
appIconUrl={appDetail.icon_url}
|
||||
appDescription={appDetail.description}
|
||||
appMode={appDetail.mode}
|
||||
appUseIconAsAnswerIcon={appDetail.use_icon_as_answer_icon}
|
||||
show={showEditModal}
|
||||
onConfirm={onEdit}
|
||||
onHide={() => setShowEditModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showDuplicateModal && (
|
||||
<DuplicateAppModal
|
||||
appName={appDetail.name}
|
||||
icon_type={appDetail.icon_type}
|
||||
icon={appDetail.icon}
|
||||
icon_background={appDetail.icon_background}
|
||||
icon_url={appDetail.icon_url}
|
||||
show={showDuplicateModal}
|
||||
onConfirm={onCopy}
|
||||
onHide={() => setShowDuplicateModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('app.deleteAppConfirmTitle')}
|
||||
content={t('app.deleteAppConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
{showImportDSLModal && (
|
||||
<UpdateDSLModal
|
||||
onCancel={() => setShowImportDSLModal(false)}
|
||||
onBackup={exportCheck}
|
||||
/>
|
||||
)}
|
||||
{secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
onConfirm={onExport}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
|
|||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex items-start p-1">
|
||||
<div className="flex items-center grow">
|
||||
{icon && icon_background && iconType === 'app' && (
|
||||
<div className='shrink-0 mr-3'>
|
||||
<AppIcon icon={icon} background={icon_background} />
|
||||
|
|
@ -71,8 +71,10 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
|
|||
|
||||
}
|
||||
{mode === 'expand' && <div className="group">
|
||||
<div className={`flex flex-row items-center text-sm font-semibold text-text-secondary group-hover:text-text-primary break-all ${textStyle?.main ?? ''}`}>
|
||||
{name}
|
||||
<div className={`flex flex-row items-center system-md-semibold text-text-secondary group-hover:text-text-primary ${textStyle?.main ?? ''}`}>
|
||||
<div className="max-w-[180px] truncate">
|
||||
{name}
|
||||
</div>
|
||||
{hoverTip
|
||||
&& <Tooltip
|
||||
popupContent={
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati
|
|||
<div
|
||||
className={`
|
||||
shrink-0
|
||||
${expand ? 'p-3' : 'p-2'}
|
||||
${expand ? 'p-2' : 'p-1'}
|
||||
`}
|
||||
>
|
||||
{iconType === 'app' && (
|
||||
|
|
|
|||
|
|
@ -24,12 +24,14 @@ type ItemProps = {
|
|||
onRemove: (id: string) => void
|
||||
readonly?: boolean
|
||||
onSave: (newDataset: DataSet) => void
|
||||
editable?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
config,
|
||||
onSave,
|
||||
onRemove,
|
||||
editable = true,
|
||||
}) => {
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
|
@ -70,22 +72,22 @@ const Item: FC<ItemProps> = ({
|
|||
<div className='grow'>
|
||||
<div className='flex items-center h-[18px]'>
|
||||
<div className='grow text-[13px] font-medium text-text-secondary truncate' title={config.name}>{config.name}</div>
|
||||
<div className='group-hover:hidden flex items-center'>
|
||||
{config.provider === 'external'
|
||||
? <Badge text={t('dataset.externalTag')}></Badge>
|
||||
: <Badge
|
||||
text={formatIndexingTechniqueAndMethod(config.indexing_technique, config.retrieval_model_dict?.search_method)}
|
||||
/>}
|
||||
{config.provider === 'external'
|
||||
? <Badge text={t('dataset.externalTag') as string} />
|
||||
: <Badge
|
||||
text={formatIndexingTechniqueAndMethod(config.indexing_technique, config.retrieval_model_dict?.search_method)}
|
||||
/>}
|
||||
</div>
|
||||
</div >
|
||||
<div className='hidden rounded-lg group-hover:flex items-center justify-end absolute right-0 top-0 bottom-0 pr-2 w-[124px] bg-gradient-to-r from-white/50 to-white to-50%'>
|
||||
{
|
||||
editable && <div
|
||||
className='flex items-center justify-center mr-1 w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
|
||||
onClick={() => setShowSettingsModal(true)}
|
||||
>
|
||||
<RiEditLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden rounded-lg group-hover:flex items-center justify-end absolute right-0 top-0 bottom-0 pr-2 w-[124px]'>
|
||||
<div
|
||||
className='flex items-center justify-center mr-1 w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
|
||||
onClick={() => setShowSettingsModal(true)}
|
||||
>
|
||||
<RiEditLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 text-text-tertiary cursor-pointer hover:text-text-destructive'
|
||||
onClick={() => onRemove(config.id)}
|
||||
|
|
@ -102,7 +104,7 @@ const Item: FC<ItemProps> = ({
|
|||
onSave={handleSave}
|
||||
/>
|
||||
</Drawer>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
|
|
@ -19,9 +19,12 @@ import {
|
|||
} from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { hasEditPermissionForDataset } from '@/utils/permission'
|
||||
|
||||
const DatasetConfig: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||
const {
|
||||
mode,
|
||||
dataSets: dataSet,
|
||||
|
|
@ -98,6 +101,20 @@ const DatasetConfig: FC = () => {
|
|||
setModelConfig(newModelConfig)
|
||||
}
|
||||
|
||||
const formattedDataset = useMemo(() => {
|
||||
return dataSet.map((item) => {
|
||||
const datasetConfig = {
|
||||
createdBy: item.created_by,
|
||||
partialMemberList: item.partial_member_list || [],
|
||||
permission: item.permission,
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
editable: hasEditPermissionForDataset(userProfile?.id || '', datasetConfig),
|
||||
}
|
||||
})
|
||||
}, [dataSet, userProfile?.id])
|
||||
|
||||
return (
|
||||
<FeaturePanel
|
||||
className='mt-2'
|
||||
|
|
@ -114,12 +131,13 @@ const DatasetConfig: FC = () => {
|
|||
{hasData
|
||||
? (
|
||||
<div className='flex flex-wrap mt-1 px-3 pb-3 justify-between'>
|
||||
{dataSet.map(item => (
|
||||
{formattedDataset.map(item => (
|
||||
<CardItem
|
||||
key={item.id}
|
||||
config={item}
|
||||
onRemove={onRemove}
|
||||
onSave={handleSave}
|
||||
editable={item.editable}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Divider from '@/app/components/base/divider'
|
|||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { type DataSet, DatasetPermission } from '@/models/datasets'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { updateDatasetSetting } from '@/service/datasets'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
|
@ -134,7 +134,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
|||
}),
|
||||
},
|
||||
} as any
|
||||
if (permission === 'partial_members') {
|
||||
if (permission === DatasetPermission.partialMembers) {
|
||||
requestParams.body.partial_member_list = selectedMemberIDs.map((id) => {
|
||||
return {
|
||||
user_id: id,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ const ChatItem: FC<ChatItemProps> = ({
|
|||
}, [modelConfig.configs.prompt_variables])
|
||||
const {
|
||||
chatList,
|
||||
chatListRef,
|
||||
isResponding,
|
||||
handleSend,
|
||||
suggestedQuestions,
|
||||
|
|
@ -102,7 +101,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
|||
query: message,
|
||||
inputs,
|
||||
model_config: configData,
|
||||
parent_message_id: getLastAnswer(chatListRef.current)?.id || null,
|
||||
parent_message_id: getLastAnswer(chatList)?.id || null,
|
||||
}
|
||||
|
||||
if ((config.file_upload as any).enabled && files?.length && supportVision)
|
||||
|
|
@ -116,7 +115,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
|||
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||
},
|
||||
)
|
||||
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, chatListRef])
|
||||
}, [appId, chatList, config, handleSend, inputs, modelAndParameter.model, modelAndParameter.parameters, modelAndParameter.provider, textGenerationModelList])
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { useChat } from '@/app/components/base/chat/chat/hooks'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import type { ChatConfig, ChatItem, OnSend } from '@/app/components/base/chat/types'
|
||||
import type { ChatConfig, ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
fetchConversationMessages,
|
||||
|
|
@ -24,13 +24,13 @@ import { useAppContext } from '@/context/app-context'
|
|||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { getLastAnswer } from '@/app/components/base/chat/utils'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
|
||||
interface DebugWithSingleModelProps {
|
||||
type DebugWithSingleModelProps = {
|
||||
checkCanSend?: () => boolean
|
||||
}
|
||||
export interface DebugWithSingleModelRefType {
|
||||
export type DebugWithSingleModelRefType = {
|
||||
handleRestart: () => void
|
||||
}
|
||||
const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSingleModelProps>(({
|
||||
|
|
@ -68,12 +68,11 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||
}, [modelConfig.configs.prompt_variables])
|
||||
const {
|
||||
chatList,
|
||||
chatListRef,
|
||||
setTargetMessageId,
|
||||
isResponding,
|
||||
handleSend,
|
||||
suggestedQuestions,
|
||||
handleStop,
|
||||
handleUpdateChatList,
|
||||
handleRestart,
|
||||
handleAnnotationAdded,
|
||||
handleAnnotationEdited,
|
||||
|
|
@ -89,7 +88,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||
)
|
||||
useFormattingChangedSubscription(chatList)
|
||||
|
||||
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
|
||||
if (checkCanSend && !checkCanSend())
|
||||
return
|
||||
const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider)
|
||||
|
|
@ -110,7 +109,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||
query: message,
|
||||
inputs,
|
||||
model_config: configData,
|
||||
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
|
||||
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
|
||||
}
|
||||
|
||||
if ((config.file_upload as any)?.enabled && files?.length && supportVision)
|
||||
|
|
@ -124,23 +123,13 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||
},
|
||||
)
|
||||
}, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList])
|
||||
}, [appId, chatList, checkCanSend, completionParams, config, handleSend, inputs, modelConfig.mode, modelConfig.model_id, modelConfig.provider, textGenerationModelList])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||
if (index === -1)
|
||||
return
|
||||
|
||||
const prevMessages = chatList.slice(0, index)
|
||||
const question = prevMessages.pop()
|
||||
const lastAnswer = getLastAnswer(prevMessages)
|
||||
|
||||
if (!question)
|
||||
return
|
||||
|
||||
handleUpdateChatList(prevMessages)
|
||||
doSend(question.content, question.message_files, lastAnswer)
|
||||
}, [chatList, handleUpdateChatList, doSend])
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
|
||||
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
|
||||
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
|
||||
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||
}, [chatList, doSend])
|
||||
|
||||
const allToolIcons = useMemo(() => {
|
||||
const icons: Record<string, any> = {}
|
||||
|
|
@ -173,6 +162,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||
inputs={inputs}
|
||||
inputsForm={inputsForm}
|
||||
onRegenerate={doRegenerate}
|
||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
||||
onStopResponding={handleStop}
|
||||
showPromptLog
|
||||
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={40} />}
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ const Configuration: FC = () => {
|
|||
}, [formattingChangedDispatcher, setShowAppConfigureFeaturesModal])
|
||||
const handleAddPromptVariable = useCallback((variable: PromptVariable[]) => {
|
||||
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
|
||||
draft.configs.prompt_variables = variable
|
||||
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...variable]
|
||||
})
|
||||
setModelConfig(newModelConfig)
|
||||
}, [modelConfig])
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
'use client'
|
||||
import type { HTMLProps } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import {
|
||||
RiLoopLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
Cog8ToothIcon,
|
||||
DocumentTextIcon,
|
||||
PaintBrushIcon,
|
||||
RocketLaunchIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiBookOpenLine,
|
||||
RiEqualizer2Line,
|
||||
RiExternalLinkLine,
|
||||
RiPaintBrushLine,
|
||||
RiWindowLine,
|
||||
} from '@remixicon/react'
|
||||
import SettingsModal from './settings'
|
||||
import EmbeddedModal from './embedded'
|
||||
import CustomizeModal from './customize'
|
||||
|
|
@ -21,35 +19,33 @@ import Tooltip from '@/app/components/base/tooltip'
|
|||
import AppBasic from '@/app/components/app-sidebar/basic'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tag from '@/app/components/base/tag'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import ShareQRCode from '@/app/components/base/qrcode'
|
||||
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
export type IAppCardProps = {
|
||||
className?: string
|
||||
appInfo: AppDetailResponse & Partial<AppSSO>
|
||||
isInPanel?: boolean
|
||||
cardType?: 'api' | 'webapp'
|
||||
customBgColor?: string
|
||||
onChangeStatus: (val: boolean) => Promise<void>
|
||||
onSaveSiteConfig?: (params: ConfigParams) => Promise<void>
|
||||
onGenerateCode?: () => Promise<void>
|
||||
}
|
||||
|
||||
const EmbedIcon = ({ className = '' }: HTMLProps<HTMLDivElement>) => {
|
||||
return <div className={`${style.codeBrowserIcon} ${className}`}></div>
|
||||
}
|
||||
|
||||
function AppCard({
|
||||
appInfo,
|
||||
isInPanel,
|
||||
cardType = 'webapp',
|
||||
customBgColor,
|
||||
onChangeStatus,
|
||||
onSaveSiteConfig,
|
||||
onGenerateCode,
|
||||
|
|
@ -69,17 +65,18 @@ function AppCard({
|
|||
const OPERATIONS_MAP = useMemo(() => {
|
||||
const operationsMap = {
|
||||
webapp: [
|
||||
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon },
|
||||
{ opName: t('appOverview.overview.appInfo.customize.entry'), opIcon: PaintBrushIcon },
|
||||
{ opName: t('appOverview.overview.appInfo.launch'), opIcon: RiExternalLinkLine },
|
||||
] as { opName: string; opIcon: any }[],
|
||||
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
|
||||
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: RiBookOpenLine }],
|
||||
app: [],
|
||||
}
|
||||
if (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow')
|
||||
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon })
|
||||
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: RiWindowLine })
|
||||
|
||||
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.customize.entry'), opIcon: RiPaintBrushLine })
|
||||
|
||||
if (isCurrentWorkspaceEditor)
|
||||
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon })
|
||||
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: RiEqualizer2Line })
|
||||
|
||||
return operationsMap
|
||||
}, [isCurrentWorkspaceEditor, appInfo, t])
|
||||
|
|
@ -97,7 +94,7 @@ function AppCard({
|
|||
|
||||
const genClickFuncByName = (opName: string) => {
|
||||
switch (opName) {
|
||||
case t('appOverview.overview.appInfo.preview'):
|
||||
case t('appOverview.overview.appInfo.launch'):
|
||||
return () => {
|
||||
window.open(appUrl, '_blank')
|
||||
}
|
||||
|
|
@ -132,45 +129,52 @@ function AppCard({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={cn('rounded-xl border-effects-highlight border-t border-l-[0.5px] bg-background-default', className)}>
|
||||
<div className={cn('px-6 py-5')}>
|
||||
<div className="mb-2.5 flex flex-row items-start justify-between">
|
||||
<AppBasic
|
||||
iconType={cardType}
|
||||
icon={appInfo.icon}
|
||||
icon_background={appInfo.icon_background}
|
||||
name={basicName}
|
||||
type={
|
||||
isApp
|
||||
? t('appOverview.overview.appInfo.explanation')
|
||||
: t('appOverview.overview.apiInfo.explanation')
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row items-center h-9">
|
||||
<Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}>
|
||||
{runningStatus
|
||||
? t('appOverview.overview.status.running')
|
||||
: t('appOverview.overview.status.disable')}
|
||||
</Tag>
|
||||
<div
|
||||
className={
|
||||
`${isInPanel ? 'border-l-[0.5px] border-t' : 'shadow-xs border-[0.5px]'} rounded-xl border-effects-highlight w-full max-w-full ${className ?? ''}`}
|
||||
>
|
||||
<div className={`${customBgColor ?? 'bg-background-default'} rounded-xl`}>
|
||||
<div className='flex flex-col p-3 justify-center items-start gap-3 self-stretch border-b-[0.5px] border-divider-subtle w-full'>
|
||||
<div className='flex items-center gap-3 self-stretch w-full'>
|
||||
<AppBasic
|
||||
iconType={cardType}
|
||||
icon={appInfo.icon}
|
||||
icon_background={appInfo.icon_background}
|
||||
name={basicName}
|
||||
type={
|
||||
isApp
|
||||
? t('appOverview.overview.appInfo.explanation')
|
||||
: t('appOverview.overview.apiInfo.explanation')
|
||||
}
|
||||
/>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Indicator color={runningStatus ? 'green' : 'yellow'} />
|
||||
<div className={`${runningStatus ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase`}>
|
||||
{runningStatus
|
||||
? t('appOverview.overview.status.running')
|
||||
: t('appOverview.overview.status.disable')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={toggleDisabled} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center py-2">
|
||||
<div className="py-1">
|
||||
<div className="pb-1 text-xs text-text-tertiary">
|
||||
<div className='flex flex-col justify-center items-start self-stretch'>
|
||||
<div className="pb-1 system-xs-medium text-text-tertiary">
|
||||
{isApp
|
||||
? t('appOverview.overview.appInfo.accessibleAddress')
|
||||
: t('appOverview.overview.apiInfo.accessibleAddress')}
|
||||
</div>
|
||||
<div className="w-full h-9 px-2 py-0.5 bg-components-input-bg-normal rounded-lg justify-start items-center inline-flex">
|
||||
<div className="h-4 px-2 justify-start items-start gap-2 flex flex-1 min-w-0">
|
||||
<div className="text-text-secondary system-xs-medium truncate">
|
||||
<div className="w-full h-9 pl-2 p-1 bg-components-input-bg-normal rounded-lg items-center inline-flex gap-0.5">
|
||||
<div className="h-4 px-1 justify-start items-start gap-2 flex flex-1 min-w-0">
|
||||
<div className="text-text-secondary text-xs font-medium text-ellipsis overflow-hidden whitespace-nowrap">
|
||||
{isApp ? appUrl : apiUrl}
|
||||
</div>
|
||||
</div>
|
||||
<Divider type="vertical" className="!h-3.5 shrink-0" />
|
||||
{isApp && <ShareQRCode content={isApp ? appUrl : apiUrl} />}
|
||||
<CopyFeedback content={isApp ? appUrl : apiUrl}/>
|
||||
<CopyFeedback
|
||||
content={isApp ? appUrl : apiUrl}
|
||||
className={'!size-6'}
|
||||
/>
|
||||
{isApp && <ShareQRCode content={isApp ? appUrl : apiUrl} className='z-50 !size-6 hover:bg-state-base-hover rounded-md' selectorId={randomString(8)} />}
|
||||
{isApp && <Divider type="vertical" className="!h-3.5 shrink-0 !mx-0.5" />}
|
||||
{/* button copy link/ button regenerate */}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
|
|
@ -189,16 +193,22 @@ function AppCard({
|
|||
<Tooltip
|
||||
popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
|
||||
>
|
||||
<ActionButton onClick={() => setShowConfirmDelete(true)}>
|
||||
<RiLoopLeftLine className={cn('w-4 h-4', genLoading && 'animate-spin')} />
|
||||
</ActionButton>
|
||||
<div
|
||||
className="w-6 h-6 cursor-pointer hover:bg-state-base-hover rounded-md"
|
||||
onClick={() => setShowConfirmDelete(true)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
`w-full h-full ${style.refreshIcon} ${genLoading ? style.generateLogo : ''}`}
|
||||
></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'pt-2 flex flex-row items-center flex-wrap gap-y-2'}>
|
||||
{!isApp && <SecretKeyButton className='shrink-0 !h-8 mr-2' textCls='!text-text-secondary font-medium' iconCls='stroke-[1.2px]' appId={appInfo.id} />}
|
||||
<div className={'flex p-3 items-center gap-1 self-stretch'}>
|
||||
{!isApp && <SecretKeyButton appId={appInfo.id} />}
|
||||
{OPERATIONS_MAP[cardType].map((op) => {
|
||||
const disabled
|
||||
= op.opName === t('appOverview.overview.appInfo.settings.entry')
|
||||
|
|
@ -206,7 +216,9 @@ function AppCard({
|
|||
: !runningStatus
|
||||
return (
|
||||
<Button
|
||||
className="mr-2"
|
||||
className="mr-1 min-w-[88px]"
|
||||
size="small"
|
||||
variant={'ghost'}
|
||||
key={op.opName}
|
||||
onClick={genClickFuncByName(op.opName)}
|
||||
disabled={disabled}
|
||||
|
|
@ -217,9 +229,9 @@ function AppCard({
|
|||
}
|
||||
popupClassName={disabled ? 'mt-[-8px]' : '!hidden'}
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<op.opIcon className="h-4 w-4 mr-1.5 stroke-[1.8px]" />
|
||||
<span className="text-[13px]">{op.opName}</span>
|
||||
<div className="flex items-center justify-center gap-[1px]">
|
||||
<op.opIcon className="h-3.5 w-3.5" />
|
||||
<div className={`${runningStatus ? 'text-text-tertiary' : 'text-components-button-ghost-text-disabled'} system-xs-medium px-[3px]`}>{op.opName}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import Chat from '../chat'
|
|||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
ChatItemInTree,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import { useChat } from '../chat/hooks'
|
||||
import { getLastAnswer } from '../utils'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
|
||||
import { useChatWithHistoryContext } from './context'
|
||||
import Header from './header'
|
||||
import ConfigPanel from './config-panel'
|
||||
|
|
@ -20,7 +21,7 @@ import AnswerIcon from '@/app/components/base/answer-icon'
|
|||
const ChatWrapper = () => {
|
||||
const {
|
||||
appParams,
|
||||
appPrevChatList,
|
||||
appPrevChatTree,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
inputsForms,
|
||||
|
|
@ -50,8 +51,7 @@ const ChatWrapper = () => {
|
|||
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
||||
const {
|
||||
chatList,
|
||||
chatListRef,
|
||||
handleUpdateChatList,
|
||||
setTargetMessageId,
|
||||
handleSend,
|
||||
handleStop,
|
||||
isResponding,
|
||||
|
|
@ -62,7 +62,7 @@ const ChatWrapper = () => {
|
|||
inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any,
|
||||
inputsForm: inputsForms,
|
||||
},
|
||||
appPrevChatList,
|
||||
appPrevChatTree,
|
||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||
)
|
||||
|
||||
|
|
@ -72,13 +72,13 @@ const ChatWrapper = () => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
|
||||
const data: any = {
|
||||
query: message,
|
||||
files,
|
||||
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
||||
conversation_id: currentConversationId,
|
||||
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
|
||||
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
|
||||
}
|
||||
|
||||
handleSend(
|
||||
|
|
@ -91,31 +91,21 @@ const ChatWrapper = () => {
|
|||
},
|
||||
)
|
||||
}, [
|
||||
chatListRef,
|
||||
chatList,
|
||||
handleNewConversationCompleted,
|
||||
handleSend,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
handleSend,
|
||||
newConversationInputs,
|
||||
handleNewConversationCompleted,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||
if (index === -1)
|
||||
return
|
||||
|
||||
const prevMessages = chatList.slice(0, index)
|
||||
const question = prevMessages.pop()
|
||||
const lastAnswer = getLastAnswer(prevMessages)
|
||||
|
||||
if (!question)
|
||||
return
|
||||
|
||||
handleUpdateChatList(prevMessages)
|
||||
doSend(question.content, question.message_files, lastAnswer)
|
||||
}, [chatList, handleUpdateChatList, doSend])
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
|
||||
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
|
||||
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
|
||||
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||
}, [chatList, doSend])
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (inputsForms.length) {
|
||||
|
|
@ -187,6 +177,7 @@ const ChatWrapper = () => {
|
|||
answerIcon={answerIcon}
|
||||
hideProcessDetail
|
||||
themeBuilder={themeBuilder}
|
||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { createContext, useContext } from 'use-context-selector'
|
|||
import type {
|
||||
Callback,
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
ChatItemInTree,
|
||||
Feedback,
|
||||
} from '../types'
|
||||
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
|
||||
|
|
@ -16,7 +16,7 @@ import type {
|
|||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
|
||||
export interface ChatWithHistoryContextValue {
|
||||
export type ChatWithHistoryContextValue = {
|
||||
appInfoError?: any
|
||||
appInfoLoading?: boolean
|
||||
appMeta?: AppMeta
|
||||
|
|
@ -25,7 +25,7 @@ export interface ChatWithHistoryContextValue {
|
|||
appChatListDataLoading?: boolean
|
||||
currentConversationId: string
|
||||
currentConversationItem?: ConversationItem
|
||||
appPrevChatList: ChatItem[]
|
||||
appPrevChatTree: ChatItemInTree[]
|
||||
pinnedConversationList: AppConversationData['data']
|
||||
conversationList: AppConversationData['data']
|
||||
showConfigPanelBeforeChat: boolean
|
||||
|
|
@ -53,7 +53,7 @@ export interface ChatWithHistoryContextValue {
|
|||
|
||||
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
|
||||
currentConversationId: '',
|
||||
appPrevChatList: [],
|
||||
appPrevChatTree: [],
|
||||
pinnedConversationList: [],
|
||||
conversationList: [],
|
||||
showConfigPanelBeforeChat: false,
|
||||
|
|
|
|||
|
|
@ -12,10 +12,13 @@ import produce from 'immer'
|
|||
import type {
|
||||
Callback,
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Feedback,
|
||||
} from '../types'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { getPrevChatList } from '../utils'
|
||||
import { buildChatItemTree } from '../utils'
|
||||
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import {
|
||||
delConversation,
|
||||
fetchAppInfo,
|
||||
|
|
@ -40,6 +43,32 @@ import { useAppFavicon } from '@/hooks/use-app-favicon'
|
|||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
function getFormattedChatList(messages: any[]) {
|
||||
const newChatList: ChatItem[] = []
|
||||
messages.forEach((item) => {
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
parentMessageId: item.parent_message_id || undefined,
|
||||
})
|
||||
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.retriever_resources,
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
parentMessageId: `question-${item.id}`,
|
||||
})
|
||||
})
|
||||
return newChatList
|
||||
}
|
||||
|
||||
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
|
||||
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
|
||||
|
|
@ -109,9 +138,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
|
||||
|
||||
const appPrevChatList = useMemo(
|
||||
const appPrevChatTree = useMemo(
|
||||
() => (currentConversationId && appChatListData?.data.length)
|
||||
? getPrevChatList(appChatListData.data)
|
||||
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
||||
: [],
|
||||
[appChatListData, currentConversationId],
|
||||
)
|
||||
|
|
@ -403,7 +432,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||
appConversationDataLoading,
|
||||
appChatListData,
|
||||
appChatListDataLoading,
|
||||
appPrevChatList,
|
||||
appPrevChatTree,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
showConfigPanelBeforeChat,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
|||
import { checkOrSetAccessToken } from '@/app/components/share/utils'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
|
||||
interface ChatWithHistoryProps {
|
||||
type ChatWithHistoryProps = {
|
||||
className?: string
|
||||
}
|
||||
const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
||||
|
|
@ -30,7 +30,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||
appInfoError,
|
||||
appData,
|
||||
appInfoLoading,
|
||||
appPrevChatList,
|
||||
appPrevChatTree,
|
||||
showConfigPanelBeforeChat,
|
||||
appChatListDataLoading,
|
||||
chatShouldReloadKey,
|
||||
|
|
@ -38,7 +38,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||
themeBuilder,
|
||||
} = useChatWithHistoryContext()
|
||||
|
||||
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length)
|
||||
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatTree.length)
|
||||
const customConfig = appData?.custom_config
|
||||
const site = appData?.site
|
||||
|
||||
|
|
@ -76,9 +76,9 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||
<HeaderInMobile />
|
||||
)
|
||||
}
|
||||
<div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatList.length && 'flex items-center justify-center'}`}>
|
||||
<div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatTree.length && 'flex items-center justify-center'}`}>
|
||||
{
|
||||
showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatList.length && (
|
||||
showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatTree.length && (
|
||||
<div className={`flex w-full items-center justify-center h-full ${isMobile && 'px-4'}`}>
|
||||
<ConfigPanel />
|
||||
</div>
|
||||
|
|
@ -99,7 +99,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||
)
|
||||
}
|
||||
|
||||
export interface ChatWithHistoryWrapProps {
|
||||
export type ChatWithHistoryWrapProps = {
|
||||
installedAppInfo?: InstalledApp
|
||||
className?: string
|
||||
}
|
||||
|
|
@ -120,7 +120,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
|||
appChatListDataLoading,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
appPrevChatList,
|
||||
appPrevChatTree,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
showConfigPanelBeforeChat,
|
||||
|
|
@ -154,7 +154,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
|||
appChatListDataLoading,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
appPrevChatList,
|
||||
appPrevChatTree,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
showConfigPanelBeforeChat,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows
|
|||
import cn from '@/utils/classnames'
|
||||
import { FileList } from '@/app/components/base/file-uploader'
|
||||
|
||||
interface AnswerProps {
|
||||
type AnswerProps = {
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
|
|
@ -209,19 +209,19 @@ const Answer: FC<AnswerProps> = ({
|
|||
}
|
||||
{item.siblingCount && item.siblingCount > 1 && item.siblingIndex !== undefined && <div className="pt-3.5 flex justify-center items-center text-sm">
|
||||
<button
|
||||
className={`${item.prevSibling ? 'opacity-100' : 'opacity-65'}`}
|
||||
className={`${item.prevSibling ? 'opacity-100' : 'opacity-30'}`}
|
||||
disabled={!item.prevSibling}
|
||||
onClick={() => item.prevSibling && switchSibling?.(item.prevSibling)}
|
||||
>
|
||||
<ChevronRight className="w-[14px] h-[14px] rotate-180 text-text-tertiary" />
|
||||
<ChevronRight className="w-[14px] h-[14px] rotate-180 text-text-primary" />
|
||||
</button>
|
||||
<span className="px-2 text-xs text-text-quaternary">{item.siblingIndex + 1} / {item.siblingCount}</span>
|
||||
<span className="px-2 text-xs text-text-primary">{item.siblingIndex + 1} / {item.siblingCount}</span>
|
||||
<button
|
||||
className={`${item.nextSibling ? 'opacity-100' : 'opacity-65'}`}
|
||||
className={`${item.nextSibling ? 'opacity-100' : 'opacity-30'}`}
|
||||
disabled={!item.nextSibling}
|
||||
onClick={() => item.nextSibling && switchSibling?.(item.nextSibling)}
|
||||
>
|
||||
<ChevronRight className="w-[14px] h-[14px] text-text-tertiary" />
|
||||
<ChevronRight className="w-[14px] h-[14px] text-text-primary" />
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
|
@ -12,8 +13,10 @@ import { v4 as uuidV4 } from 'uuid'
|
|||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
ChatItemInTree,
|
||||
Inputs,
|
||||
} from '../types'
|
||||
import { getThreadMessages } from '../utils'
|
||||
import type { InputForm } from './type'
|
||||
import {
|
||||
getProcessedInputs,
|
||||
|
|
@ -33,7 +36,7 @@ import {
|
|||
} from '@/app/components/base/file-uploader/utils'
|
||||
|
||||
type GetAbortController = (abortController: AbortController) => void
|
||||
interface SendCallback {
|
||||
type SendCallback = {
|
||||
onGetConversationMessages?: (conversationId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
onConversationComplete?: (conversationId: string) => void
|
||||
|
|
@ -46,7 +49,7 @@ export const useChat = (
|
|||
inputs: Inputs
|
||||
inputsForm: InputForm[]
|
||||
},
|
||||
prevChatList?: ChatItem[],
|
||||
prevChatTree?: ChatItemInTree[],
|
||||
stopChat?: (taskId: string) => void,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -56,14 +59,48 @@ export const useChat = (
|
|||
const hasStopResponded = useRef(false)
|
||||
const [isResponding, setIsResponding] = useState(false)
|
||||
const isRespondingRef = useRef(false)
|
||||
const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
|
||||
const chatListRef = useRef<ChatItem[]>(prevChatList || [])
|
||||
const taskIdRef = useRef('')
|
||||
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
|
||||
const conversationMessagesAbortControllerRef = useRef<AbortController | null>(null)
|
||||
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
|
||||
const params = useParams()
|
||||
const pathname = usePathname()
|
||||
|
||||
const [chatTree, setChatTree] = useState<ChatItemInTree[]>(prevChatTree || [])
|
||||
const chatTreeRef = useRef<ChatItemInTree[]>(chatTree)
|
||||
const [targetMessageId, setTargetMessageId] = useState<string>()
|
||||
const threadMessages = useMemo(() => getThreadMessages(chatTree, targetMessageId), [chatTree, targetMessageId])
|
||||
|
||||
const getIntroduction = useCallback((str: string) => {
|
||||
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
|
||||
}, [formSettings?.inputs, formSettings?.inputsForm])
|
||||
|
||||
/** Final chat list that will be rendered */
|
||||
const chatList = useMemo(() => {
|
||||
const ret = [...threadMessages]
|
||||
if (config?.opening_statement) {
|
||||
const index = threadMessages.findIndex(item => item.isOpeningStatement)
|
||||
|
||||
if (index > -1) {
|
||||
ret[index] = {
|
||||
...ret[index],
|
||||
content: getIntroduction(config.opening_statement),
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret.unshift({
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(config.opening_statement),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
})
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
|
||||
|
||||
useEffect(() => {
|
||||
setAutoFreeze(false)
|
||||
return () => {
|
||||
|
|
@ -71,43 +108,50 @@ export const useChat = (
|
|||
}
|
||||
}, [])
|
||||
|
||||
const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
|
||||
setChatList(newChatList)
|
||||
chatListRef.current = newChatList
|
||||
/** Find the target node by bfs and then operate on it */
|
||||
const produceChatTreeNode = useCallback((targetId: string, operation: (node: ChatItemInTree) => void) => {
|
||||
return produce(chatTreeRef.current, (draft) => {
|
||||
const queue: ChatItemInTree[] = [...draft]
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
if (current.id === targetId) {
|
||||
operation(current)
|
||||
break
|
||||
}
|
||||
if (current.children)
|
||||
queue.push(...current.children)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
type UpdateChatTreeNode = {
|
||||
(id: string, fields: Partial<ChatItemInTree>): void
|
||||
(id: string, update: (node: ChatItemInTree) => void): void
|
||||
}
|
||||
|
||||
const updateChatTreeNode: UpdateChatTreeNode = useCallback((
|
||||
id: string,
|
||||
fieldsOrUpdate: Partial<ChatItemInTree> | ((node: ChatItemInTree) => void),
|
||||
) => {
|
||||
const nextState = produceChatTreeNode(id, (node) => {
|
||||
if (typeof fieldsOrUpdate === 'function') {
|
||||
fieldsOrUpdate(node)
|
||||
}
|
||||
else {
|
||||
Object.keys(fieldsOrUpdate).forEach((key) => {
|
||||
(node as any)[key] = (fieldsOrUpdate as any)[key]
|
||||
})
|
||||
}
|
||||
})
|
||||
setChatTree(nextState)
|
||||
chatTreeRef.current = nextState
|
||||
}, [produceChatTreeNode])
|
||||
|
||||
const handleResponding = useCallback((isResponding: boolean) => {
|
||||
setIsResponding(isResponding)
|
||||
isRespondingRef.current = isResponding
|
||||
}, [])
|
||||
|
||||
const getIntroduction = useCallback((str: string) => {
|
||||
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
|
||||
}, [formSettings?.inputs, formSettings?.inputsForm])
|
||||
useEffect(() => {
|
||||
if (config?.opening_statement) {
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const index = draft.findIndex(item => item.isOpeningStatement)
|
||||
|
||||
if (index > -1) {
|
||||
draft[index] = {
|
||||
...draft[index],
|
||||
content: getIntroduction(config.opening_statement),
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
}
|
||||
}
|
||||
else {
|
||||
draft.unshift({
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(config.opening_statement),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
}, [config?.opening_statement, getIntroduction, config?.suggested_questions, handleUpdateChatList])
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
hasStopResponded.current = true
|
||||
handleResponding(false)
|
||||
|
|
@ -123,50 +167,50 @@ export const useChat = (
|
|||
conversationId.current = ''
|
||||
taskIdRef.current = ''
|
||||
handleStop()
|
||||
const newChatList = config?.opening_statement
|
||||
? [{
|
||||
id: `${Date.now()}`,
|
||||
content: config.opening_statement,
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions,
|
||||
}]
|
||||
: []
|
||||
handleUpdateChatList(newChatList)
|
||||
setChatTree([])
|
||||
setSuggestQuestions([])
|
||||
}, [
|
||||
config,
|
||||
handleStop,
|
||||
handleUpdateChatList,
|
||||
])
|
||||
}, [handleStop])
|
||||
|
||||
const updateCurrentQA = useCallback(({
|
||||
const updateCurrentQAOnTree = useCallback(({
|
||||
parentId,
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
}: {
|
||||
parentId?: string
|
||||
responseItem: ChatItem
|
||||
questionId: string
|
||||
placeholderAnswerId: string
|
||||
placeholderQuestionId: string
|
||||
questionItem: ChatItem
|
||||
}) => {
|
||||
const newListWithAnswer = produce(
|
||||
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({ ...responseItem })
|
||||
let nextState: ChatItemInTree[]
|
||||
const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] }
|
||||
if (!parentId && !chatTree.some(item => [placeholderQuestionId, questionItem.id].includes(item.id))) {
|
||||
// QA whose parent is not provided is considered as a first message of the conversation,
|
||||
// and it should be a root node of the chat tree
|
||||
nextState = produce(chatTree, (draft) => {
|
||||
draft.push(currentQA)
|
||||
})
|
||||
handleUpdateChatList(newListWithAnswer)
|
||||
}, [handleUpdateChatList])
|
||||
}
|
||||
else {
|
||||
// find the target QA in the tree and update it; if not found, insert it to its parent node
|
||||
nextState = produceChatTreeNode(parentId!, (parentNode) => {
|
||||
const questionNodeIndex = parentNode.children!.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
|
||||
if (questionNodeIndex === -1)
|
||||
parentNode.children!.push(currentQA)
|
||||
else
|
||||
parentNode.children![questionNodeIndex] = currentQA
|
||||
})
|
||||
}
|
||||
setChatTree(nextState)
|
||||
chatTreeRef.current = nextState
|
||||
}, [chatTree, produceChatTreeNode])
|
||||
|
||||
const handleSend = useCallback(async (
|
||||
url: string,
|
||||
data: {
|
||||
query: string
|
||||
files?: FileEntity[]
|
||||
parent_message_id?: string
|
||||
[key: string]: any
|
||||
},
|
||||
{
|
||||
|
|
@ -183,12 +227,15 @@ export const useChat = (
|
|||
return false
|
||||
}
|
||||
|
||||
const questionId = `question-${Date.now()}`
|
||||
const parentMessage = threadMessages.find(item => item.id === data.parent_message_id)
|
||||
|
||||
const placeholderQuestionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
id: placeholderQuestionId,
|
||||
content: data.query,
|
||||
isAnswer: false,
|
||||
message_files: data.files,
|
||||
parentMessageId: data.parent_message_id,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
|
|
@ -196,18 +243,27 @@ export const useChat = (
|
|||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
parentMessageId: questionItem.id,
|
||||
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
|
||||
}
|
||||
|
||||
const newList = [...chatListRef.current, questionItem, placeholderAnswerItem]
|
||||
handleUpdateChatList(newList)
|
||||
setTargetMessageId(parentMessage?.id)
|
||||
updateCurrentQAOnTree({
|
||||
parentId: data.parent_message_id,
|
||||
responseItem: placeholderAnswerItem,
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
})
|
||||
|
||||
// answer
|
||||
const responseItem: ChatItem = {
|
||||
const responseItem: ChatItemInTree = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
agent_thoughts: [],
|
||||
message_files: [],
|
||||
isAnswer: true,
|
||||
parentMessageId: questionItem.id,
|
||||
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
|
||||
}
|
||||
|
||||
handleResponding(true)
|
||||
|
|
@ -268,7 +324,9 @@ export const useChat = (
|
|||
}
|
||||
|
||||
if (messageId && !hasSetResponseId) {
|
||||
questionItem.id = `question-${messageId}`
|
||||
responseItem.id = messageId
|
||||
responseItem.parentMessageId = questionItem.id
|
||||
hasSetResponseId = true
|
||||
}
|
||||
|
||||
|
|
@ -279,11 +337,11 @@ export const useChat = (
|
|||
if (messageId)
|
||||
responseItem.id = messageId
|
||||
|
||||
updateCurrentQA({
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
|
|
@ -304,43 +362,32 @@ export const useChat = (
|
|||
if (!newResponseItem)
|
||||
return
|
||||
|
||||
const newChatList = produce(chatListRef.current, (draft) => {
|
||||
const index = draft.findIndex(item => item.id === responseItem.id)
|
||||
if (index !== -1) {
|
||||
const question = draft[index - 1]
|
||||
draft[index - 1] = {
|
||||
...question,
|
||||
}
|
||||
draft[index] = {
|
||||
...draft[index],
|
||||
content: newResponseItem.answer,
|
||||
log: [
|
||||
...newResponseItem.message,
|
||||
...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
|
||||
? [
|
||||
{
|
||||
role: 'assistant',
|
||||
text: newResponseItem.answer,
|
||||
files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
more: {
|
||||
time: formatTime(newResponseItem.created_at, 'hh:mm A'),
|
||||
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
|
||||
latency: newResponseItem.provider_response_latency.toFixed(2),
|
||||
},
|
||||
// for agent log
|
||||
conversationId: conversationId.current,
|
||||
input: {
|
||||
inputs: newResponseItem.inputs,
|
||||
query: newResponseItem.query,
|
||||
},
|
||||
}
|
||||
}
|
||||
updateChatTreeNode(responseItem.id, {
|
||||
content: newResponseItem.answer,
|
||||
log: [
|
||||
...newResponseItem.message,
|
||||
...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
|
||||
? [
|
||||
{
|
||||
role: 'assistant',
|
||||
text: newResponseItem.answer,
|
||||
files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
more: {
|
||||
time: formatTime(newResponseItem.created_at, 'hh:mm A'),
|
||||
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
|
||||
latency: newResponseItem.provider_response_latency.toFixed(2),
|
||||
},
|
||||
// for agent log
|
||||
conversationId: conversationId.current,
|
||||
input: {
|
||||
inputs: newResponseItem.inputs,
|
||||
query: newResponseItem.query,
|
||||
},
|
||||
})
|
||||
handleUpdateChatList(newChatList)
|
||||
}
|
||||
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
|
||||
try {
|
||||
|
|
@ -360,11 +407,11 @@ export const useChat = (
|
|||
if (lastThought)
|
||||
responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file]
|
||||
|
||||
updateCurrentQA({
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
},
|
||||
onThought(thought) {
|
||||
|
|
@ -372,6 +419,7 @@ export const useChat = (
|
|||
const response = responseItem as any
|
||||
if (thought.message_id && !hasSetResponseId)
|
||||
response.id = thought.message_id
|
||||
|
||||
if (response.agent_thoughts.length === 0) {
|
||||
response.agent_thoughts.push(thought)
|
||||
}
|
||||
|
|
@ -387,11 +435,11 @@ export const useChat = (
|
|||
responseItem.agent_thoughts!.push(thought)
|
||||
}
|
||||
}
|
||||
updateCurrentQA({
|
||||
responseItem,
|
||||
questionId,
|
||||
placeholderAnswerId,
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
},
|
||||
onMessageEnd: (messageEnd) => {
|
||||
|
|
@ -401,43 +449,36 @@ export const useChat = (
|
|||
id: messageEnd.metadata.annotation_reply.id,
|
||||
authorName: messageEnd.metadata.annotation_reply.account.name,
|
||||
})
|
||||
const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId)
|
||||
const newListWithAnswer = produce(
|
||||
baseState,
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({
|
||||
...responseItem,
|
||||
})
|
||||
})
|
||||
handleUpdateChatList(newListWithAnswer)
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
return
|
||||
}
|
||||
responseItem.citation = messageEnd.metadata?.retriever_resources || []
|
||||
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
|
||||
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
|
||||
|
||||
const newListWithAnswer = produce(
|
||||
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
handleUpdateChatList(newListWithAnswer)
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
responseItem.content = messageReplace.answer
|
||||
},
|
||||
onError() {
|
||||
handleResponding(false)
|
||||
const newChatList = produce(chatListRef.current, (draft) => {
|
||||
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
handleUpdateChatList(newChatList)
|
||||
},
|
||||
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
|
||||
taskIdRef.current = task_id
|
||||
|
|
@ -446,89 +487,84 @@ export const useChat = (
|
|||
status: WorkflowRunningStatus.Running,
|
||||
tracing: [],
|
||||
}
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
},
|
||||
onWorkflowFinished: ({ data }) => {
|
||||
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
onWorkflowFinished: ({ data: workflowFinishedData }) => {
|
||||
responseItem.workflowProcess!.status = workflowFinishedData.status as WorkflowRunningStatus
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
},
|
||||
onIterationStart: ({ data }) => {
|
||||
onIterationStart: ({ data: iterationStartedData }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
...iterationStartedData,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
} as any)
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
},
|
||||
onIterationFinish: ({ data }) => {
|
||||
onIterationFinish: ({ data: iterationFinishedData }) => {
|
||||
const tracing = responseItem.workflowProcess!.tracing!
|
||||
const iterationIndex = tracing.findIndex(item => item.node_id === data.node_id
|
||||
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
|
||||
const iterationIndex = tracing.findIndex(item => item.node_id === iterationFinishedData.node_id
|
||||
&& (item.execution_metadata?.parallel_id === iterationFinishedData.execution_metadata?.parallel_id || item.parallel_id === iterationFinishedData.execution_metadata?.parallel_id))!
|
||||
tracing[iterationIndex] = {
|
||||
...tracing[iterationIndex],
|
||||
...data,
|
||||
...iterationFinishedData,
|
||||
status: WorkflowRunningStatus.Succeeded,
|
||||
} as any
|
||||
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
},
|
||||
onNodeStarted: ({ data }) => {
|
||||
if (data.iteration_id)
|
||||
onNodeStarted: ({ data: nodeStartedData }) => {
|
||||
if (nodeStartedData.iteration_id)
|
||||
return
|
||||
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
...nodeStartedData,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
} as any)
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
},
|
||||
onNodeFinished: ({ data }) => {
|
||||
if (data.iteration_id)
|
||||
onNodeFinished: ({ data: nodeFinishedData }) => {
|
||||
if (nodeFinishedData.iteration_id)
|
||||
return
|
||||
|
||||
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => {
|
||||
if (!item.execution_metadata?.parallel_id)
|
||||
return item.node_id === data.node_id
|
||||
return item.node_id === nodeFinishedData.node_id
|
||||
|
||||
return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata.parallel_id)
|
||||
return item.node_id === nodeFinishedData.node_id && (item.execution_metadata?.parallel_id === nodeFinishedData.execution_metadata.parallel_id)
|
||||
})
|
||||
responseItem.workflowProcess!.tracing[currentIndex] = nodeFinishedData as any
|
||||
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: data.parent_message_id,
|
||||
})
|
||||
responseItem.workflowProcess!.tracing[currentIndex] = data as any
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
},
|
||||
onTTSChunk: (messageId: string, audio: string) => {
|
||||
if (!audio || audio === '')
|
||||
|
|
@ -542,11 +578,13 @@ export const useChat = (
|
|||
})
|
||||
return true
|
||||
}, [
|
||||
config?.suggested_questions_after_answer,
|
||||
updateCurrentQA,
|
||||
t,
|
||||
chatTree.length,
|
||||
threadMessages,
|
||||
config?.suggested_questions_after_answer,
|
||||
updateCurrentQAOnTree,
|
||||
updateChatTreeNode,
|
||||
notify,
|
||||
handleUpdateChatList,
|
||||
handleResponding,
|
||||
formatTime,
|
||||
params.token,
|
||||
|
|
@ -556,76 +594,61 @@ export const useChat = (
|
|||
])
|
||||
|
||||
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
|
||||
handleUpdateChatList(chatListRef.current.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: answer,
|
||||
annotation: {
|
||||
...item.annotation,
|
||||
logAnnotation: undefined,
|
||||
} as any,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [handleUpdateChatList])
|
||||
const targetQuestionId = chatList[index - 1].id
|
||||
const targetAnswerId = chatList[index].id
|
||||
|
||||
updateChatTreeNode(targetQuestionId, {
|
||||
content: query,
|
||||
})
|
||||
updateChatTreeNode(targetAnswerId, {
|
||||
content: answer,
|
||||
annotation: {
|
||||
...chatList[index].annotation,
|
||||
logAnnotation: undefined,
|
||||
} as any,
|
||||
})
|
||||
}, [chatList, updateChatTreeNode])
|
||||
|
||||
const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
|
||||
handleUpdateChatList(chatListRef.current.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
const answerItem = {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: {
|
||||
id: annotationId,
|
||||
authorName,
|
||||
logAnnotation: {
|
||||
content: answer,
|
||||
account: {
|
||||
id: '',
|
||||
name: authorName,
|
||||
email: '',
|
||||
},
|
||||
},
|
||||
} as Annotation,
|
||||
}
|
||||
return answerItem
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [handleUpdateChatList])
|
||||
const handleAnnotationRemoved = useCallback((index: number) => {
|
||||
handleUpdateChatList(chatListRef.current.map((item, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: {
|
||||
...(item.annotation || {}),
|
||||
const targetQuestionId = chatList[index - 1].id
|
||||
const targetAnswerId = chatList[index].id
|
||||
|
||||
updateChatTreeNode(targetQuestionId, {
|
||||
content: query,
|
||||
})
|
||||
|
||||
updateChatTreeNode(targetAnswerId, {
|
||||
content: chatList[index].content,
|
||||
annotation: {
|
||||
id: annotationId,
|
||||
authorName,
|
||||
logAnnotation: {
|
||||
content: answer,
|
||||
account: {
|
||||
id: '',
|
||||
} as Annotation,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [handleUpdateChatList])
|
||||
name: authorName,
|
||||
email: '',
|
||||
},
|
||||
},
|
||||
} as Annotation,
|
||||
})
|
||||
}, [chatList, updateChatTreeNode])
|
||||
|
||||
const handleAnnotationRemoved = useCallback((index: number) => {
|
||||
const targetAnswerId = chatList[index].id
|
||||
|
||||
updateChatTreeNode(targetAnswerId, {
|
||||
content: chatList[index].content,
|
||||
annotation: {
|
||||
...(chatList[index].annotation || {}),
|
||||
id: '',
|
||||
} as Annotation,
|
||||
})
|
||||
}, [chatList, updateChatTreeNode])
|
||||
|
||||
return {
|
||||
chatList,
|
||||
chatListRef,
|
||||
handleUpdateChatList,
|
||||
setTargetMessageId,
|
||||
conversationId: conversationId.current,
|
||||
isResponding,
|
||||
setIsResponding,
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import Chat from '../chat'
|
|||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
ChatItemInTree,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import { useChat } from '../chat/hooks'
|
||||
import { getLastAnswer } from '../utils'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
|
||||
import { useEmbeddedChatbotContext } from './context'
|
||||
import ConfigPanel from './config-panel'
|
||||
import { isDify } from './utils'
|
||||
|
|
@ -51,13 +52,12 @@ const ChatWrapper = () => {
|
|||
} as ChatConfig
|
||||
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
||||
const {
|
||||
chatListRef,
|
||||
chatList,
|
||||
setTargetMessageId,
|
||||
handleSend,
|
||||
handleStop,
|
||||
isResponding,
|
||||
suggestedQuestions,
|
||||
handleUpdateChatList,
|
||||
} = useChat(
|
||||
appConfig,
|
||||
{
|
||||
|
|
@ -71,15 +71,15 @@ const ChatWrapper = () => {
|
|||
useEffect(() => {
|
||||
if (currentChatInstanceRef.current)
|
||||
currentChatInstanceRef.current.handleStop = handleStop
|
||||
}, [])
|
||||
}, [currentChatInstanceRef, handleStop])
|
||||
|
||||
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
|
||||
const data: any = {
|
||||
query: message,
|
||||
files,
|
||||
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
||||
conversation_id: currentConversationId,
|
||||
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
|
||||
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
|
||||
}
|
||||
|
||||
handleSend(
|
||||
|
|
@ -92,32 +92,21 @@ const ChatWrapper = () => {
|
|||
},
|
||||
)
|
||||
}, [
|
||||
chatListRef,
|
||||
appConfig,
|
||||
chatList,
|
||||
handleNewConversationCompleted,
|
||||
handleSend,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
handleSend,
|
||||
newConversationInputs,
|
||||
handleNewConversationCompleted,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||
if (index === -1)
|
||||
return
|
||||
|
||||
const prevMessages = chatList.slice(0, index)
|
||||
const question = prevMessages.pop()
|
||||
const lastAnswer = getLastAnswer(prevMessages)
|
||||
|
||||
if (!question)
|
||||
return
|
||||
|
||||
handleUpdateChatList(prevMessages)
|
||||
doSend(question.content, question.message_files, lastAnswer)
|
||||
}, [chatList, handleUpdateChatList, doSend])
|
||||
const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
|
||||
const question = chatList.find(item => item.id === chatItem.parentMessageId)!
|
||||
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
|
||||
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||
}, [chatList, doSend])
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (inputsForms.length) {
|
||||
|
|
@ -172,6 +161,7 @@ const ChatWrapper = () => {
|
|||
answerIcon={answerIcon}
|
||||
hideProcessDetail
|
||||
themeBuilder={themeBuilder}
|
||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ import { useLocalStorageState } from 'ahooks'
|
|||
import produce from 'immer'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Feedback,
|
||||
} from '../types'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { getPrevChatList, getProcessedInputsFromUrlParams } from '../utils'
|
||||
import { buildChatItemTree, getProcessedInputsFromUrlParams } from '../utils'
|
||||
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
|
||||
import {
|
||||
fetchAppInfo,
|
||||
fetchAppMeta,
|
||||
|
|
@ -32,6 +34,33 @@ import { useToastContext } from '@/app/components/base/toast'
|
|||
import { changeLanguage } from '@/i18n/i18next-config'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
|
||||
function getFormattedChatList(messages: any[]) {
|
||||
const newChatList: ChatItem[] = []
|
||||
messages.forEach((item) => {
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
parentMessageId: item.parent_message_id || undefined,
|
||||
})
|
||||
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.retriever_resources,
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
parentMessageId: `question-${item.id}`,
|
||||
})
|
||||
})
|
||||
return newChatList
|
||||
}
|
||||
|
||||
export const useEmbeddedChatbot = () => {
|
||||
const isInstalledApp = false
|
||||
|
|
@ -77,7 +106,7 @@ export const useEmbeddedChatbot = () => {
|
|||
|
||||
const appPrevChatList = useMemo(
|
||||
() => (currentConversationId && appChatListData?.data.length)
|
||||
? getPrevChatList(appChatListData.data)
|
||||
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
||||
: [],
|
||||
[appChatListData, currentConversationId],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,32 +14,32 @@ export type {
|
|||
PromptVariable,
|
||||
} from '@/models/debug'
|
||||
|
||||
export interface UserInputForm {
|
||||
export type UserInputForm = {
|
||||
default: string
|
||||
label: string
|
||||
required: boolean
|
||||
variable: string
|
||||
}
|
||||
|
||||
export interface UserInputFormTextInput {
|
||||
export type UserInputFormTextInput = {
|
||||
'text-input': UserInputForm & {
|
||||
max_length: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface UserInputFormSelect {
|
||||
export type UserInputFormSelect = {
|
||||
select: UserInputForm & {
|
||||
options: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface UserInputFormParagraph {
|
||||
export type UserInputFormParagraph = {
|
||||
paragraph: UserInputForm
|
||||
}
|
||||
|
||||
export type VisionConfig = VisionSettings
|
||||
|
||||
export interface EnableType {
|
||||
export type EnableType = {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ export type ChatConfig = Omit<ModelConfig, 'model'> & {
|
|||
supportCitationHitInfo?: boolean
|
||||
}
|
||||
|
||||
export interface WorkflowProcess {
|
||||
export type WorkflowProcess = {
|
||||
status: WorkflowRunningStatus
|
||||
tracing: NodeTracing[]
|
||||
expand?: boolean // for UI
|
||||
|
|
@ -67,16 +67,19 @@ export type ChatItem = IChatItem & {
|
|||
|
||||
export type ChatItemInTree = {
|
||||
children?: ChatItemInTree[]
|
||||
} & IChatItem
|
||||
} & ChatItem
|
||||
|
||||
export type OnSend = (message: string, files?: FileEntity[], last_answer?: ChatItem | null) => void
|
||||
export type OnSend = {
|
||||
(message: string, files?: FileEntity[]): void
|
||||
(message: string, files: FileEntity[] | undefined, isRegenerate: boolean, lastAnswer?: ChatItem | null): void
|
||||
}
|
||||
|
||||
export type OnRegenerate = (chatItem: ChatItem) => void
|
||||
|
||||
export interface Callback {
|
||||
export type Callback = {
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
export interface Feedback {
|
||||
export type Feedback = {
|
||||
rating: 'like' | 'dislike' | null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
|
||||
import { UUID_NIL } from './constants'
|
||||
import type { IChatItem } from './chat/type'
|
||||
import type { ChatItem, ChatItemInTree } from './types'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
|
||||
async function decodeBase64AndDecompress(base64String: string) {
|
||||
const binaryString = atob(base64String)
|
||||
|
|
@ -21,67 +19,24 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
|
|||
return inputs
|
||||
}
|
||||
|
||||
function getLastAnswer(chatList: ChatItem[]) {
|
||||
function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
|
||||
return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
|
||||
}
|
||||
|
||||
function getLastAnswer<T extends ChatItem | ChatItemInTree>(chatList: T[]): T | null {
|
||||
for (let i = chatList.length - 1; i >= 0; i--) {
|
||||
const item = chatList[i]
|
||||
if (item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement)
|
||||
if (isValidGeneratedAnswer(item))
|
||||
return item
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function appendQAToChatList(chatList: ChatItem[], item: any) {
|
||||
// we append answer first and then question since will reverse the whole chatList later
|
||||
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||
chatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.retriever_resources,
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
})
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
chatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the latest thread messages from all messages of the conversation.
|
||||
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
|
||||
*
|
||||
* @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
|
||||
* @returns An array of ChatItems representing the latest thread.
|
||||
* Build a chat item tree from a chat list
|
||||
* @param allMessages - The chat list, sorted from oldest to newest
|
||||
* @returns The chat item tree
|
||||
*/
|
||||
function getPrevChatList(fetchedMessages: any[]) {
|
||||
const ret: ChatItem[] = []
|
||||
let nextMessageId = null
|
||||
|
||||
for (const item of fetchedMessages) {
|
||||
if (!item.parent_message_id) {
|
||||
appendQAToChatList(ret, item)
|
||||
break
|
||||
}
|
||||
|
||||
if (!nextMessageId) {
|
||||
appendQAToChatList(ret, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
else {
|
||||
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||
appendQAToChatList(ret, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret.reverse()
|
||||
}
|
||||
|
||||
function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
|
||||
const map: Record<string, ChatItemInTree> = {}
|
||||
const rootNodes: ChatItemInTree[] = []
|
||||
|
|
@ -208,7 +163,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
|
|||
|
||||
export {
|
||||
getProcessedInputsFromUrlParams,
|
||||
getPrevChatList,
|
||||
isValidGeneratedAnswer,
|
||||
getLastAnswer,
|
||||
buildChatItemTree,
|
||||
getThreadMessages,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { Fragment, type ReactNode } from 'react'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type ContentDialogProps = {
|
||||
className?: string
|
||||
show: boolean
|
||||
onClose?: () => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const ContentDialog = ({
|
||||
className,
|
||||
show,
|
||||
onClose,
|
||||
children,
|
||||
}: ContentDialogProps) => {
|
||||
return (
|
||||
<Transition
|
||||
show={show}
|
||||
as="div"
|
||||
className="absolute left-0 top-0 w-full h-full z-20 p-2 box-border"
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 inset-0 w-full bg-app-detail-overlay-bg"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Transition.Child>
|
||||
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transform transition ease-out duration-300"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transform transition ease-in duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<div className={classNames(
|
||||
'absolute left-0 w-full bg-app-detail-bg border-r border-divider-burn',
|
||||
className,
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContentDialog
|
||||
|
|
@ -76,15 +76,13 @@ export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className'
|
|||
}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 cursor-pointer hover:bg-components-button-ghost-bg-hover rounded-lg ${
|
||||
className ?? ''
|
||||
className={`w-8 h-8 cursor-pointer hover:bg-components-button-ghost-bg-hover rounded-lg ${className ?? ''
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
onClick={onClickCopy}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={`w-full h-full ${copyStyle.copyIcon} ${
|
||||
isCopied ? copyStyle.copied : ''
|
||||
className={`w-full h-full ${copyStyle.copyIcon} ${isCopied ? copyStyle.copied : ''
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ export const useFile = (fileConfig: FileUpload) => {
|
|||
type: '',
|
||||
size: 0,
|
||||
progress: 0,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
transferMethod: TransferMethod.remote_url,
|
||||
supportFileType: '',
|
||||
url,
|
||||
isRemote: true,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import RehypeRaw from 'rehype-raw'
|
|||
import SyntaxHighlighter from 'react-syntax-highlighter'
|
||||
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
|
||||
import { Component, createContext, memo, useContext, useMemo, useRef, useState } from 'react'
|
||||
import { flow } from 'lodash/fp'
|
||||
import cn from '@/utils/classnames'
|
||||
import CopyBtn from '@/app/components/base/copy-btn'
|
||||
import SVGBtn from '@/app/components/base/svg'
|
||||
|
|
@ -59,9 +60,24 @@ const getCorrectCapitalizationLanguageName = (language: string) => {
|
|||
const preprocessLaTeX = (content?: string) => {
|
||||
if (typeof content !== 'string')
|
||||
return content
|
||||
return content.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`)
|
||||
.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`)
|
||||
.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`)
|
||||
|
||||
return flow([
|
||||
(str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`),
|
||||
(str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`),
|
||||
(str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`),
|
||||
])(content)
|
||||
}
|
||||
|
||||
const preprocessThinkTag = (content: string) => {
|
||||
if (!content.trim().startsWith('<think>\n'))
|
||||
return content
|
||||
|
||||
return flow([
|
||||
(str: string) => str.replace('<think>\n', '<details open><summary class="text-gray-500 font-bold">Thinking</summary><div class="text-gray-500 p-3 ml-2 bg-gray-50 border-l border-gray-300">\n'),
|
||||
(str: string) => str.includes('\n</think>')
|
||||
? str.replace('\n</think>', '\n</div></details>')
|
||||
: `${str}\n</div></details>`,
|
||||
])(content)
|
||||
}
|
||||
|
||||
export function PreCode(props: { children: any }) {
|
||||
|
|
@ -239,11 +255,18 @@ const Link: Components['a'] = ({ node, ...props }) => {
|
|||
}
|
||||
|
||||
export function Markdown(props: { content: string; className?: string }) {
|
||||
const latexContent = preprocessLaTeX(props.content)
|
||||
const latexContent = flow([
|
||||
preprocessThinkTag,
|
||||
preprocessLaTeX,
|
||||
])(props.content)
|
||||
return (
|
||||
<div className={cn('markdown-body', props.className)}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
|
||||
remarkPlugins={[
|
||||
RemarkGfm,
|
||||
[RemarkMath, { singleDollarTextMath: false }],
|
||||
RemarkBreaks,
|
||||
]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
RehypeRaw as any,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ import {
|
|||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export interface PromptEditorProps {
|
||||
export type PromptEditorProps = {
|
||||
instanceId?: string
|
||||
compact?: boolean
|
||||
className?: string
|
||||
|
|
@ -149,7 +149,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
||||
<div className='relative min-h-5'>
|
||||
<RichTextPlugin
|
||||
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`} style={style || {}} />}
|
||||
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-text-secondary`} style={style || {}} />}
|
||||
placeholder={<Placeholder value={placeholder} className={cn('truncate', placeholderClassName)} compact={compact} />}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export const useVariableOptions = (
|
|||
return (
|
||||
<VariableMenuItem
|
||||
title={item.value}
|
||||
icon={<BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />}
|
||||
icon={<BracketsX className='w-[14px] h-[14px] text-text-accent' />}
|
||||
queryString={queryString}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
|
|
@ -162,7 +162,7 @@ export const useVariableOptions = (
|
|||
return (
|
||||
<VariableMenuItem
|
||||
title={t('common.promptEditor.variable.modal.add')}
|
||||
icon={<BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />}
|
||||
icon={<BracketsX className='w-[14px] h-[14px] text-text-accent' />}
|
||||
queryString={queryString}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
|
|
@ -211,7 +211,7 @@ export const useExternalToolOptions = (
|
|||
background={item.icon_background}
|
||||
/>
|
||||
}
|
||||
extraElement={<div className='text-xs text-gray-400'>{item.variableName}</div>}
|
||||
extraElement={<div className='text-xs text-text-tertiary'>{item.variableName}</div>}
|
||||
queryString={queryString}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
|
|
@ -240,8 +240,8 @@ export const useExternalToolOptions = (
|
|||
return (
|
||||
<VariableMenuItem
|
||||
title={t('common.promptEditor.variable.modal.addTool')}
|
||||
icon={<Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />}
|
||||
extraElement={< ArrowUpRight className='w-3 h-3 text-gray-400' />}
|
||||
icon={<Tool03 className='w-[14px] h-[14px] text-text-accent' />}
|
||||
extraElement={< ArrowUpRight className='w-3 h-3 text-text-tertiary' />}
|
||||
queryString={queryString}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import type { PickerBlockMenuOption } from './menu'
|
|||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
interface ComponentPickerProps {
|
||||
type ComponentPickerProps = {
|
||||
triggerString: string
|
||||
contextBlock?: ContextBlockType
|
||||
queryBlock?: QueryBlockType
|
||||
|
|
@ -135,7 +135,7 @@ const ComponentPicker = ({
|
|||
// See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
|
||||
<div className='w-0 h-0'>
|
||||
<div
|
||||
className='p-1 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'
|
||||
className='p-1 w-[260px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'
|
||||
style={{
|
||||
...floatingStyles,
|
||||
visibility: isPositioned ? 'visible' : 'hidden',
|
||||
|
|
@ -148,7 +148,7 @@ const ComponentPicker = ({
|
|||
{
|
||||
// Divider
|
||||
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||
<div className='h-px bg-gray-100 my-1 w-full -translate-x-1'></div>
|
||||
<div className='h-px bg-divider-subtle my-1 w-full -translate-x-1'></div>
|
||||
)
|
||||
}
|
||||
{option.renderMenuOption({
|
||||
|
|
@ -169,7 +169,7 @@ const ComponentPicker = ({
|
|||
<>
|
||||
{
|
||||
(!!options.length) && (
|
||||
<div className='h-px bg-gray-100 my-1 w-full -translate-x-1'></div>
|
||||
<div className='h-px bg-divider-subtle my-1 w-full -translate-x-1'></div>
|
||||
)
|
||||
}
|
||||
<div className='p-1'>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ export const PromptMenuItem = memo(({
|
|||
return (
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50 rounded-md
|
||||
${isSelected && !disabled && '!bg-gray-50'}
|
||||
${disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
|
||||
flex items-center px-3 h-6 cursor-pointer hover:bg-state-base-hover rounded-md
|
||||
${isSelected && !disabled && '!bg-state-base-hover'}
|
||||
${disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-state-base-hover cursor-pointer'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={setRefElement}
|
||||
|
|
@ -38,7 +38,7 @@ export const PromptMenuItem = memo(({
|
|||
onClick()
|
||||
}}>
|
||||
{icon}
|
||||
<div className='ml-1 text-[13px] text-gray-900'>{title}</div>
|
||||
<div className='ml-1 text-[13px] text-text-secondary'>{title}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue