mirror of
https://github.com/langgenius/dify.git
synced 2026-02-03 02:16:49 +08:00
Compare commits
37 Commits
1.0.0
...
fix/expose
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a32589f61 | |||
| 2e467cbc74 | |||
| 561a3bf7a0 | |||
| 83cad07fb3 | |||
| 4f6a4f244c | |||
| cef49da576 | |||
| e16453591e | |||
| 450319790e | |||
| 4b783037d3 | |||
| d04f40c274 | |||
| 9ab4f35b84 | |||
| 4668c4996a | |||
| 330dc2fd44 | |||
| 96eed571d9 | |||
| 24d80000ac | |||
| 7ff527293a | |||
| d88b2dd198 | |||
| 66654faef3 | |||
| c8de30f3d9 | |||
| f0fb38fed4 | |||
| 43ab7c22a7 | |||
| 829cd70889 | |||
| 972421efe2 | |||
| 98ada40532 | |||
| 931d704612 | |||
| bb4e7da720 | |||
| 64e122c5f6 | |||
| d0d0bf570e | |||
| e53052ab7a | |||
| cd46ebbb34 | |||
| 8d4136d864 | |||
| 4125e575af | |||
| 7259c0d69f | |||
| ce2dd22bd7 | |||
| 1eb072fd43 | |||
| f49b0822aa | |||
| de824d3713 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1,5 +1,5 @@
|
||||
# Ensure that .sh scripts use LF as line separator, even if they are checked out
|
||||
# to Windows(NTFS) file-system, by a user of Docker for Window.
|
||||
# to Windows(NTFS) file-system, by a user of Docker for Windows.
|
||||
# These .sh scripts will be run from the Container after `docker compose up -d`.
|
||||
# If they appear to be CRLF style, Dash from the Container will fail to execute
|
||||
# them.
|
||||
|
||||
155
CONTRIBUTING_DE.md
Normal file
155
CONTRIBUTING_DE.md
Normal file
@ -0,0 +1,155 @@
|
||||
# MITWIRKEN
|
||||
|
||||
So, du möchtest zu Dify beitragen – das ist großartig, wir können es kaum erwarten, zu sehen, was du beisteuern wirst. Als ein Startup mit begrenzter Mitarbeiterzahl und Finanzierung haben wir große Ambitionen, den intuitivsten Workflow zum Aufbau und zur Verwaltung von LLM-Anwendungen zu entwickeln. Jede Unterstützung aus der Community zählt wirklich.
|
||||
|
||||
Dieser Leitfaden, ebenso wie Dify selbst, ist ein ständig in Entwicklung befindliches Projekt. Wir schätzen Ihr Verständnis, falls er zeitweise hinter dem tatsächlichen Projekt zurückbleibt, und freuen uns über jegliches Feedback, das uns hilft, ihn zu verbessern.
|
||||
|
||||
Bezüglich der Lizenzierung nehmen Sie sich bitte einen Moment Zeit, um unser kurzes [License and Contributor Agreement](./LICENSE) zu lesen. Die Community hält sich außerdem an den [Code of Conduct](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Bevor Sie loslegen
|
||||
|
||||
[Finde](https://github.com/langgenius/dify/issues?q=is:issue+is:open) ein bestehendes Issue, oder [öffne](https://github.com/langgenius/dify/issues/new/choose) ein neues. Wir kategorisieren Issues in zwei Typen:
|
||||
|
||||
### Feature-Anfragen
|
||||
|
||||
* Wenn Sie eine neue Feature-Anfrage stellen, bitten wir Sie zu erklären, was das vorgeschlagene Feature bewirken soll und so viel Kontext wie möglich bereitzustellen. [@perzeusss](https://github.com/perzeuss) hat einen soliden [Feature Request Copilot](https://udify.app/chat/MK2kVSnw1gakVwMX) entwickelt, der Ihnen dabei hilft, Ihre Anforderungen zu formulieren. Probieren Sie ihn gerne aus.
|
||||
|
||||
* Wenn Sie eines der bestehenden Issues übernehmen möchten, hinterlassen Sie einfach einen Kommentar darunter, in dem Sie uns dies mitteilen.
|
||||
|
||||
Ein Teammitglied, das in der entsprechenden Richtung arbeitet, wird hinzugezogen. Wenn alles in Ordnung ist, gibt es das Okay, mit der Codierung zu beginnen. Wir bitten Sie, mit der Umsetzung des Features zu warten, damit keine Ihrer Arbeiten verloren gehen sollte unsererseits Änderungen vorgeschlagen werden.
|
||||
|
||||
Je nachdem, in welchen Bereich das vorgeschlagene Feature fällt, können Sie mit verschiedenen Teammitgliedern sprechen. Hier ist eine Übersicht der Bereiche, an denen unsere Teammitglieder derzeit arbeiten:
|
||||
|
||||
| Member | Scope |
|
||||
| ------------------------------------------------------------ | ---------------------------------------------------- |
|
||||
| [@yeuoly](https://github.com/Yeuoly) | Architecting Agents |
|
||||
| [@jyong](https://github.com/JohnJyong) | RAG pipeline design |
|
||||
| [@GarfieldDai](https://github.com/GarfieldDai) | Building workflow orchestrations |
|
||||
| [@iamjoel](https://github.com/iamjoel) & [@zxhlyh](https://github.com/zxhlyh) | Making our frontend a breeze to use |
|
||||
| [@guchenhe](https://github.com/guchenhe) & [@crazywoola](https://github.com/crazywoola) | Developer experience, points of contact for anything |
|
||||
| [@takatost](https://github.com/takatost) | Overall product direction and architecture |
|
||||
|
||||
Wie wir Prioritäten setzen:
|
||||
|
||||
| Feature Type | Priority |
|
||||
| ------------------------------------------------------------ | --------------- |
|
||||
| Funktionen mit hoher Priorität, wie sie von einem Teammitglied gekennzeichnet wurden | High Priority |
|
||||
| Beliebte Funktionsanfragen von unserem [Community-Feedback-Board](https://github.com/langgenius/dify/discussions/categories/feedbacks) | Medium Priority |
|
||||
| Nicht-Kernfunktionen und kleinere Verbesserungen | Low Priority |
|
||||
| Wertvoll, aber nicht unmittelbar | Future-Feature |
|
||||
|
||||
### Sonstiges (e.g. bug report, performance optimization, typo correction)
|
||||
|
||||
* Fangen Sie sofort an zu programmieren..
|
||||
|
||||
Wie wir Prioritäten setzen:
|
||||
|
||||
| Issue Type | Priority |
|
||||
| ------------------------------------------------------------ | --------------- |
|
||||
| Fehler in Kernfunktionen (Anmeldung nicht möglich, Anwendungen funktionieren nicht, Sicherheitslücken) | Critical |
|
||||
| Nicht-kritische Fehler, Leistungsverbesserungen | Medium Priority |
|
||||
| Kleinere Fehlerkorrekturen (Schreibfehler, verwirrende, aber funktionierende Benutzeroberfläche) | Low Priority |
|
||||
|
||||
## Installieren
|
||||
|
||||
Hier sind die Schritte, um Dify für die Entwicklung einzurichten:
|
||||
|
||||
### 1. Fork dieses Repository
|
||||
|
||||
### 2. Clone das Repo
|
||||
|
||||
Klonen Sie das geforkte Repository von Ihrem Terminal aus:
|
||||
|
||||
```shell
|
||||
git clone git@github.com:<github_username>/dify.git
|
||||
```
|
||||
|
||||
### 3. Abhängigkeiten prüfen
|
||||
|
||||
Dify benötigt die folgenden Abhängigkeiten zum Bauen – stellen Sie sicher, dass sie auf Ihrem System installiert sind:
|
||||
|
||||
* [Docker](https://www.docker.com/)
|
||||
* [Docker Compose](https://docs.docker.com/compose/install/)
|
||||
* [Node.js v18.x (LTS)](http://nodejs.org)
|
||||
* [pnpm](https://pnpm.io/)
|
||||
* [Python](https://www.python.org/) version 3.11.x or 3.12.x
|
||||
|
||||
### 4. Installationen
|
||||
|
||||
Dify setzt sich aus einem Backend und einem Frontend zusammen. Wechseln Sie in das Backend-Verzeichnis mit `cd api/` und folgen Sie der [Backend README](api/README.md) zur Installation. Öffnen Sie in einem separaten Terminal das Frontend-Verzeichnis mit `cd web/` und folgen Sie der [Frontend README](web/README.md) zur Installation.
|
||||
|
||||
Überprüfen Sie die [Installation FAQ](https://docs.dify.ai/learn-more/faq/install-faq) für eine Liste bekannter Probleme und Schritte zur Fehlerbehebung.
|
||||
|
||||
### 5. Besuchen Sie dify in Ihrem Browser
|
||||
|
||||
Um Ihre Einrichtung zu validieren, öffnen Sie Ihren Browser und navigieren Sie zu [http://localhost:3000](http://localhost:3000) (Standardwert oder Ihre selbst konfigurierte URL und Port). Sie sollten nun Dify im laufenden Betrieb sehen.
|
||||
|
||||
## Entwickeln
|
||||
|
||||
Wenn Sie einen Modellanbieter hinzufügen, ist [dieser Leitfaden](https://github.com/langgenius/dify/blob/main/api/core/model_runtime/README.md) für Sie.
|
||||
|
||||
Wenn Sie einen Tool-Anbieter für Agent oder Workflow hinzufügen möchten, ist [dieser Leitfaden](./api/core/tools/README.md) für Sie.
|
||||
|
||||
Um Ihnen eine schnelle Orientierung zu bieten, wo Ihr Beitrag passt, folgt eine kurze, kommentierte Übersicht des Backends und Frontends von Dify:
|
||||
|
||||
### Backend
|
||||
|
||||
Dify’s Backend ist in Python geschrieben und nutzt [Flask](https://flask.palletsprojects.com/en/3.0.x/) als Web-Framework. Es verwendet [SQLAlchemy](https://www.sqlalchemy.org/) für ORM und [Celery](https://docs.celeryq.dev/en/stable/getting-started/introduction.html) für Task-Queueing. Die Autorisierungslogik erfolgt über Flask-login.
|
||||
|
||||
```text
|
||||
[api/]
|
||||
├── constants // Konstante Einstellungen, die in der gesamten Codebasis verwendet werden.
|
||||
├── controllers // API-Routendefinitionen und Logik zur Bearbeitung von Anfragen.
|
||||
├── core // Orchestrierung von Kernanwendungen, Modellintegrationen und Tools.
|
||||
├── docker // Konfigurationen im Zusammenhang mit Docker und Containerisierung.
|
||||
├── events // Ereignisbehandlung und -verarbeitung
|
||||
├── extensions // Erweiterungen mit Frameworks/Plattformen von Drittanbietern.
|
||||
├── fields // Felddefinitionen für die Serialisierung/Marshalling.
|
||||
├── libs // Wiederverwendbare Bibliotheken und Hilfsprogramme
|
||||
├── migrations // Skripte für die Datenbankmigration.
|
||||
├── models // Datenbankmodelle und Schemadefinitionen.
|
||||
├── services // Gibt die Geschäftslogik an.
|
||||
├── storage // Speicherung privater Schlüssel.
|
||||
├── tasks // Handhabung von asynchronen Aufgaben und Hintergrundaufträgen.
|
||||
└── tests
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
Die Website basiert auf einem [Next.js](https://nextjs.org/)-Boilerplate in TypeScript und verwendet [Tailwind CSS](https://tailwindcss.com/) für das Styling. [React-i18next](https://react.i18next.com/) wird für die Internationalisierung genutzt.
|
||||
|
||||
```text
|
||||
[web/]
|
||||
├── app // Layouts, Seiten und Komponenten
|
||||
│ ├── (commonLayout) // gemeinsames Layout für die gesamte Anwendung
|
||||
│ ├── (shareLayout) // Layouts, die speziell für tokenspezifische Sitzungen gemeinsam genutzt werden
|
||||
│ ├── activate // Seite aufrufen
|
||||
│ ├── components // gemeinsam genutzt von Seiten und Layouts
|
||||
│ ├── install // Seite installieren
|
||||
│ ├── signin // Anmeldeseite
|
||||
│ └── styles // global geteilte Stile
|
||||
├── assets // Statische Vermögenswerte
|
||||
├── bin // Skripte, die beim Build-Schritt ausgeführt werden
|
||||
├── config // einstellbare Einstellungen und Optionen
|
||||
├── context // gemeinsame Kontexte, die von verschiedenen Teilen der Anwendung verwendet werden
|
||||
├── dictionaries // Sprachspezifische Übersetzungsdateien
|
||||
├── docker // Container-Konfigurationen
|
||||
├── hooks // Wiederverwendbare Haken
|
||||
├── i18n // Konfiguration der Internationalisierung
|
||||
├── models // beschreibt Datenmodelle und Formen von API-Antworten
|
||||
├── public // Meta-Assets wie Favicon
|
||||
├── service // legt Formen von API-Aktionen fest
|
||||
├── test
|
||||
├── types // Beschreibungen von Funktionsparametern und Rückgabewerten
|
||||
└── utils // Gemeinsame Nutzenfunktionen
|
||||
```
|
||||
|
||||
## Einreichung Ihrer PR
|
||||
|
||||
Am Ende ist es Zeit, einen Pull Request (PR) in unserem Repository zu eröffnen. Für wesentliche Features mergen wir diese zunächst in den `deploy/dev`-Branch zum Testen, bevor sie in den `main`-Branch übernommen werden. Falls Sie auf Probleme wie Merge-Konflikte stoßen oder nicht wissen, wie man einen Pull Request erstellt, schauen Sie sich [GitHub's Pull Request Tutorial](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) an.
|
||||
|
||||
Und das war's! Sobald Ihr PR gemerged wurde, werden Sie als Mitwirkender in unserem [README](https://github.com/langgenius/dify/blob/main/README.md) aufgeführt.
|
||||
|
||||
## Hilfe bekommen
|
||||
|
||||
Wenn Sie beim Beitragen jemals nicht weiter wissen oder eine brennende Frage haben, richten Sie Ihre Anfrage einfach über das entsprechende GitHub-Issue an uns oder besuchen Sie unseren [Discord](https://discord.gg/8Tpq4AcN9c) für ein kurzes Gespräch.
|
||||
@ -49,6 +49,8 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_DE.md"><img alt="README in Deutsch" src="https://img.shields.io/badge/German-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
36
README_AR.md
36
README_AR.md
@ -45,6 +45,7 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
<div style="text-align: right;">
|
||||
@ -53,8 +54,7 @@
|
||||
|
||||
**1. سير العمل**: قم ببناء واختبار سير عمل الذكاء الاصطناعي القوي على قماش بصري، مستفيدًا من جميع الميزات التالية وأكثر.
|
||||
|
||||
https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa
|
||||
|
||||
<https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa>
|
||||
|
||||
**2. الدعم الشامل للنماذج**: تكامل سلس مع مئات من LLMs الخاصة / مفتوحة المصدر من عشرات من موفري التحليل والحلول المستضافة ذاتيًا، مما يغطي GPT و Mistral و Llama3 وأي نماذج متوافقة مع واجهة OpenAI API. يمكن العثور على قائمة كاملة بمزودي النموذج المدعومين [هنا](https://docs.dify.ai/getting-started/readme/model-providers).
|
||||
|
||||
@ -69,7 +69,9 @@
|
||||
**6. الـ LLMOps**: راقب وتحلل سجلات التطبيق والأداء على مر الزمن. يمكنك تحسين الأوامر والبيانات والنماذج باستمرار استنادًا إلى البيانات الإنتاجية والتعليقات.
|
||||
|
||||
**7.الواجهة الخلفية (Backend) كخدمة**: تأتي جميع عروض Dify مع APIs مطابقة، حتى يمكنك دمج Dify بسهولة في منطق أعمالك الخاص.
|
||||
|
||||
## مقارنة الميزات
|
||||
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<th align="center">الميزة</th>
|
||||
@ -136,8 +138,8 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## استخدام Dify
|
||||
|
||||
- **سحابة </br>**
|
||||
نحن نستضيف [خدمة Dify Cloud](https://dify.ai) لأي شخص لتجربتها بدون أي إعدادات. توفر كل قدرات النسخة التي تمت استضافتها ذاتيًا، وتتضمن 200 أمر GPT-4 مجانًا في خطة الصندوق الرملي.
|
||||
|
||||
@ -147,15 +149,19 @@
|
||||
|
||||
- **مشروع Dify للشركات / المؤسسات</br>**
|
||||
نحن نوفر ميزات إضافية مركزة على الشركات. [جدول اجتماع معنا](https://cal.com/guchenhe/30min) أو [أرسل لنا بريدًا إلكترونيًا](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry) لمناقشة احتياجات الشركات. </br>
|
||||
|
||||
> بالنسبة للشركات الناشئة والشركات الصغيرة التي تستخدم خدمات AWS، تحقق من [Dify Premium على AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) ونشرها في شبكتك الخاصة على AWS VPC بنقرة واحدة. إنها عرض AMI بأسعار معقولة مع خيار إنشاء تطبيقات بشعار وعلامة تجارية مخصصة.
|
||||
>
|
||||
## البقاء قدمًا
|
||||
|
||||
قم بإضافة نجمة إلى Dify على GitHub وتلق تنبيهًا فوريًا بالإصدارات الجديدة.
|
||||
|
||||

|
||||
|
||||
## البداية السريعة
|
||||
>
|
||||
> قبل تثبيت Dify، تأكد من أن جهازك يلبي الحد الأدنى من متطلبات النظام التالية:
|
||||
>
|
||||
>
|
||||
>- معالج >= 2 نواة
|
||||
>- ذاكرة وصول عشوائي (RAM) >= 4 جيجابايت
|
||||
|
||||
@ -188,24 +194,26 @@ docker compose up -d
|
||||
انشر Dify إلى منصة السحابة بنقرة واحدة باستخدام [terraform](https://www.terraform.io/)
|
||||
|
||||
##### Azure Global
|
||||
|
||||
- [Azure Terraform بواسطة @nikawang](https://github.com/nikawang/dify-azure-terraform)
|
||||
|
||||
##### Google Cloud
|
||||
|
||||
- [Google Cloud Terraform بواسطة @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||
|
||||
#### استخدام AWS CDK للنشر
|
||||
|
||||
انشر Dify على AWS باستخدام [CDK](https://aws.amazon.com/cdk/)
|
||||
|
||||
##### AWS
|
||||
##### AWS
|
||||
|
||||
- [AWS CDK بواسطة @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
|
||||
## المساهمة
|
||||
|
||||
لأولئك الذين يرغبون في المساهمة، انظر إلى [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) لدينا.
|
||||
لأولئك الذين يرغبون في المساهمة، انظر إلى [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) لدينا.
|
||||
في الوقت نفسه، يرجى النظر في دعم Dify عن طريق مشاركته على وسائل التواصل الاجتماعي وفي الفعاليات والمؤتمرات.
|
||||
|
||||
|
||||
> نحن نبحث عن مساهمين لمساعدة في ترجمة Dify إلى لغات أخرى غير اللغة الصينية المندرين أو الإنجليزية. إذا كنت مهتمًا بالمساعدة، يرجى الاطلاع على [README للترجمة](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) لمزيد من المعلومات، واترك لنا تعليقًا في قناة `global-users` على [خادم المجتمع على Discord](https://discord.gg/8Tpq4AcN9c).
|
||||
|
||||
**المساهمون**
|
||||
@ -215,26 +223,26 @@ docker compose up -d
|
||||
</a>
|
||||
|
||||
## المجتمع والاتصال
|
||||
* [مناقشة Github](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة.
|
||||
* [المشكلات على GitHub](https://github.com/langgenius/dify/issues). الأفضل لـ: الأخطاء التي تواجهها في استخدام Dify.AI، واقتراحات الميزات. انظر [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||
* [Discord](https://discord.gg/FngNHpbcY7). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.
|
||||
* [تويتر](https://twitter.com/dify_ai). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.
|
||||
- [مناقشة Github](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة.
|
||||
- [المشكلات على GitHub](https://github.com/langgenius/dify/issues). الأفضل لـ: الأخطاء التي تواجهها في استخدام Dify.AI، واقتراحات الميزات. انظر [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||
- [Discord](https://discord.gg/FngNHpbcY7). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.
|
||||
- [تويتر](https://twitter.com/dify_ai). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.
|
||||
|
||||
## تاريخ النجمة
|
||||
|
||||
[](https://star-history.com/#langgenius/dify&Date)
|
||||
|
||||
|
||||
## الكشف عن الأمان
|
||||
|
||||
لحماية خصوصيتك، يرجى تجنب نشر مشكلات الأمان على GitHub. بدلاً من ذلك، أرسل أسئلتك إلى security@dify.ai وسنقدم لك إجابة أكثر تفصيلاً.
|
||||
لحماية خصوصيتك، يرجى تجنب نشر مشكلات الأمان على GitHub. بدلاً من ذلك، أرسل أسئلتك إلى <security@dify.ai> وسنقدم لك إجابة أكثر تفصيلاً.
|
||||
|
||||
## الرخصة
|
||||
|
||||
هذا المستودع متاح تحت [رخصة البرنامج الحر Dify](LICENSE)، والتي تعتبر بشكل أساسي Apache 2.0 مع بعض القيود الإضافية.
|
||||
|
||||
## الكشف عن الأمان
|
||||
|
||||
لحماية خصوصيتك، يرجى تجنب نشر مشكلات الأمان على GitHub. بدلاً من ذلك، أرسل أسئلتك إلى security@dify.ai وسنقدم لك إجابة أكثر تفصيلاً.
|
||||
لحماية خصوصيتك، يرجى تجنب نشر مشكلات الأمان على GitHub. بدلاً من ذلك، أرسل أسئلتك إلى <security@dify.ai> وسنقدم لك إجابة أكثر تفصيلاً.
|
||||
|
||||
## الرخصة
|
||||
|
||||
|
||||
258
README_BN.md
Normal file
258
README_BN.md
Normal file
@ -0,0 +1,258 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">ডিফাই ওয়ার্কফ্লো ফাইল আপলোড পরিচিতি: গুগল নোটবুক-এলএম পডকাস্ট পুনর্নির্মাণ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud.dify.ai">ডিফাই ক্লাউড</a> ·
|
||||
<a href="https://docs.dify.ai/getting-started/install-self-hosted">সেল্ফ-হোস্টিং</a> ·
|
||||
<a href="https://docs.dify.ai">ডকুমেন্টেশন</a> ·
|
||||
<a href="https://udify.app/chat/22L1zSxg6yW1cWQg">ব্যাবসায়িক অনুসন্ধান</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://dify.ai" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Product-F04438"></a>
|
||||
<a href="https://dify.ai/pricing" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/free-pricing?logo=free&color=%20%23155EEF&label=pricing&labelColor=%20%23528bff"></a>
|
||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||
alt="chat on Discord"></a>
|
||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
||||
alt="join Reddit"></a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||
alt="follow on X(Twitter)"></a>
|
||||
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||
alt="follow on LinkedIn"></a>
|
||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langgenius/dify-web?labelColor=%20%23FDB062&color=%20%23f79009"></a>
|
||||
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||
<img alt="Commits last month" src="https://img.shields.io/github/commit-activity/m/langgenius/dify?labelColor=%20%2332b583&color=%20%2312b76a"></a>
|
||||
<a href="https://github.com/langgenius/dify/" target="_blank">
|
||||
<img alt="Issues closed" src="https://img.shields.io/github/issues-search?query=repo%3Alanggenius%2Fdify%20is%3Aclosed&label=issues%20closed&labelColor=%20%237d89b0&color=%20%235d6b98"></a>
|
||||
<a href="https://github.com/langgenius/dify/discussions/" target="_blank">
|
||||
<img alt="Discussion posts" src="https://img.shields.io/github/discussions/langgenius/dify?labelColor=%20%239b8afb&color=%20%237a5af8"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md"><img alt="README in English" src="https://img.shields.io/badge/English-d9d9d9"></a>
|
||||
<a href="./README_CN.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
|
||||
<a href="./README_JA.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
|
||||
<a href="./README_ES.md"><img alt="README en Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
|
||||
<a href="./README_FR.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-d9d9d9"></a>
|
||||
<a href="./README_KL.md"><img alt="README tlhIngan Hol" src="https://img.shields.io/badge/Klingon-d9d9d9"></a>
|
||||
<a href="./README_KR.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_DE.md"><img alt="README in Deutsch" src="https://img.shields.io/badge/German-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
ডিফাই একটি ওপেন-সোর্স LLM অ্যাপ ডেভেলপমেন্ট প্ল্যাটফর্ম। এটি ইন্টুইটিভ ইন্টারফেস, এজেন্টিক AI ওয়ার্কফ্লো, RAG পাইপলাইন, এজেন্ট ক্যাপাবিলিটি, মডেল ম্যানেজমেন্ট, মনিটরিং সুবিধা এবং আরও অনেক কিছু একত্রিত করে, যা দ্রুত প্রোটোটাইপ থেকে প্রোডাকশন পর্যন্ত নিয়ে যেতে সহায়তা করে।
|
||||
|
||||
## কুইক স্টার্ট
|
||||
>
|
||||
> ডিফাই ইনস্টল করার আগে, নিশ্চিত করুন যে আপনার মেশিন নিম্নলিখিত ন্যূনতম কনফিগারেশনের প্রয়োজনীয়তা পূরন করে :
|
||||
>
|
||||
>- সিপিউ >= 2 কোর
|
||||
>- র্যাম >= 4 জিবি
|
||||
|
||||
</br>
|
||||
|
||||
ডিফাই সার্ভার চালু করার সবচেয়ে সহজ উপায় [docker compose](docker/docker-compose.yaml) মাধ্যমে। নিম্নলিখিত কমান্ডগুলো ব্যবহার করে ডিফাই চালানোর আগে, নিশ্চিত করুন যে আপনার মেশিনে [Docker](https://docs.docker.com/get-docker/) এবং [Docker Compose](https://docs.docker.com/compose/install/) ইনস্টল করা আছে :
|
||||
```bash
|
||||
cd dify
|
||||
cd docker
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
চালানোর পর, আপনি আপনার ব্রাউজারে [http://localhost/install](http://localhost/install)-এ ডিফাই ড্যাশবোর্ডে অ্যাক্সেস করতে পারেন এবং ইনিশিয়ালাইজেশন প্রক্রিয়া শুরু করতে পারেন।
|
||||
|
||||
#### সাহায্যের খোঁজে
|
||||
|
||||
ডিফাই সেট আপ করতে সমস্যা হলে দয়া করে আমাদের [FAQ](https://docs.dify.ai/getting-started/install-self-hosted/faqs) দেখুন। যদি তবুও সমস্যা থেকে থাকে, তাহলে [কমিউনিটি এবং আমাদের](#community--contact) সাথে যোগাযোগ করুন।
|
||||
|
||||
> যদি আপনি ডিফাইতে অবদান রাখতে বা অতিরিক্ত উন্নয়ন করতে চান, আমাদের [সোর্স কোড থেকে ডিপ্লয়মেন্টের গাইড](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code) দেখুন।
|
||||
|
||||
## প্রধান ফিচারসমূহ
|
||||
|
||||
**১. ওয়ার্কফ্লো**:
|
||||
ভিজ্যুয়াল ক্যানভাসে AI ওয়ার্কফ্লো তৈরি এবং পরীক্ষা করুন, নিম্নলিখিত সব ফিচার এবং তার বাইরেও আরও অনেক কিছু ব্যবহার করে।
|
||||
|
||||
<https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa>
|
||||
|
||||
**২. মডেল সাপোর্ট**:
|
||||
GPT, Mistral, Llama3, এবং যেকোনো OpenAI API-সামঞ্জস্যপূর্ণ মডেলসহ, কয়েক ডজন ইনফারেন্স প্রদানকারী এবং সেল্ফ-হোস্টেড সমাধান থেকে শুরু করে প্রোপ্রাইটরি/ওপেন-সোর্স LLM-এর সাথে সহজে ইন্টিগ্রেশন। সমর্থিত মডেল প্রদানকারীদের একটি সম্পূর্ণ তালিকা পাওয়া যাবে [এখানে](https://docs.dify.ai/getting-started/readme/model-providers)।
|
||||
|
||||

|
||||
|
||||
**3. প্রম্পট IDE**:
|
||||
প্রম্পট তৈরি, মডেলের পারফরম্যান্স তুলনা এবং চ্যাট-বেজড অ্যাপে টেক্সট-টু-স্পিচের মতো বৈশিষ্ট্য যুক্ত করার জন্য ইন্টুইটিভ ইন্টারফেস।
|
||||
|
||||
**4. RAG পাইপলাইন**:
|
||||
ডকুমেন্ট ইনজেশন থেকে শুরু করে রিট্রিভ পর্যন্ত সবকিছুই বিস্তৃত RAG ক্যাপাবিলিটির আওতাভুক্ত। PDF, PPT এবং অন্যান্য সাধারণ ডকুমেন্ট ফর্ম্যাট থেকে টেক্সট এক্সট্রাকশনের জন্য আউট-অফ-বক্স সাপোর্ট।
|
||||
|
||||
**5. এজেন্ট ক্যাপাবিলিটি**:
|
||||
LLM ফাংশন কলিং বা ReAct উপর ভিত্তি করে এজেন্ট ডিফাইন করতে পারেন এবং এজেন্টের জন্য পূর্ব-নির্মিত বা কাস্টম টুলস যুক্ত করতে পারেন। Dify AI এজেন্টদের জন্য 50+ বিল্ট-ইন টুলস সরবরাহ করে, যেমন Google Search, DALL·E, Stable Diffusion এবং WolframAlpha।
|
||||
|
||||
**6. এলএলএম-অপ্স**:
|
||||
সময়ের সাথে সাথে অ্যাপ্লিকেশন লগ এবং পারফরম্যান্স মনিটর এবং বিশ্লেষণ করুন। প্রডাকশন ডেটা এবং annotation এর উপর ভিত্তি করে প্রম্পট, ডেটাসেট এবং মডেলগুলিকে ক্রমাগত উন্নত করতে পারেন।
|
||||
|
||||
**7. ব্যাকএন্ড-অ্যাজ-এ-সার্ভিস**:
|
||||
ডিফাই-এর সমস্ত অফার সংশ্লিষ্ট API-সহ আছে, যাতে আপনি অনায়াসে ডিফাইকে আপনার নিজস্ব বিজনেস লজিকে ইন্টেগ্রেট করতে পারেন।
|
||||
|
||||
## বৈশিষ্ট্য তুলনা
|
||||
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<th align="center">বৈশিষ্ট্য</th>
|
||||
<th align="center">Dify.AI</th>
|
||||
<th align="center">LangChain</th>
|
||||
<th align="center">Flowise</th>
|
||||
<th align="center">OpenAI Assistants API</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">প্রোগ্রামিং পদ্ধতি</td>
|
||||
<td align="center">API + App-oriented</td>
|
||||
<td align="center">Python Code</td>
|
||||
<td align="center">App-oriented</td>
|
||||
<td align="center">API-oriented</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">সাপোর্টেড LLMs</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
<td align="center">OpenAI-only</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">RAG ইঞ্জিন</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">এজেন্ট</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">ওয়ার্কফ্লো</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">অবজার্ভেবল</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">এন্টারপ্রাইজ ফিচার (SSO/Access control)</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">লোকাল ডেপ্লয়মেন্ট</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## ডিফাই-এর ব্যবহার
|
||||
|
||||
- **ক্লাউড </br>**
|
||||
জিরো সেটাপে ব্যবহার করতে আমাদের [Dify Cloud](https://dify.ai) সার্ভিসটি ব্যবহার করতে পারেন। এখানে সেল্ফহোস্টিং-এর সকল ফিচার ও ক্যাপাবিলিটিসহ স্যান্ডবক্সে ২০০ জিপিটি-৪ কল ফ্রি পাবেন।
|
||||
|
||||
- **সেল্ফহোস্টিং ডিফাই কমিউনিটি সংস্করণ</br>**
|
||||
সেল্ফহোস্ট করতে এই [স্টার্টার গাইড](#quick-start) ব্যবহার করে দ্রুত আপনার এনভায়রনমেন্টে ডিফাই চালান।
|
||||
আরো ইন-ডেপথ রেফারেন্সের জন্য [ডকুমেন্টেশন](https://docs.dify.ai) দেখেন।
|
||||
|
||||
- **এন্টারপ্রাইজ / প্রতিষ্ঠানের জন্য Dify</br>**
|
||||
আমরা এন্টারপ্রাইজ/প্রতিষ্ঠান-কেন্দ্রিক সেবা প্রদান করে থাকি । [এই চ্যাটবটের মাধ্যমে আপনার প্রশ্নগুলি আমাদের জন্য লগ করুন।](https://udify.app/chat/22L1zSxg6yW1cWQg) অথবা [আমাদের ইমেল পাঠান](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry) আপনার চাহিদা সম্পর্কে আলোচনা করার জন্য। </br>
|
||||
|
||||
> AWS ব্যবহারকারী স্টার্টআপ এবং ছোট ব্যবসার জন্য, [AWS মার্কেটপ্লেসে Dify Premium](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) দেখুন এবং এক-ক্লিকের মাধ্যমে এটি আপনার নিজস্ব AWS VPC-তে ডিপ্লয় করুন। এটি একটি সাশ্রয়ী মূল্যের AMI অফার, যাতে কাস্টম লোগো এবং ব্র্যান্ডিং সহ অ্যাপ তৈরির সুবিধা আছে।
|
||||
|
||||
## এগিয়ে থাকুন
|
||||
|
||||
GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন এবং নতুন রিলিজের খবর তাৎক্ষণিকভাবে পান।
|
||||
|
||||

|
||||
|
||||
## Advanced Setup
|
||||
|
||||
যদি আপনার কনফিগারেশনটি কাস্টমাইজ করার প্রয়োজন হয়, তাহলে অনুগ্রহ করে আমাদের [.env.example](docker/.env.example) ফাইল দেখুন এবং আপনার `.env` ফাইলে সংশ্লিষ্ট মানগুলি আপডেট করুন। এছাড়াও, আপনার নির্দিষ্ট এনভায়রনমেন্ট এবং প্রয়োজনীয়তার উপর ভিত্তি করে আপনাকে `docker-compose.yaml` ফাইলে সমন্বয় করতে হতে পারে, যেমন ইমেজ ভার্সন পরিবর্তন করা, পোর্ট ম্যাপিং করা, অথবা ভলিউম মাউন্ট করা।
|
||||
যেকোনো পরিবর্তন করার পর, অনুগ্রহ করে `docker-compose up -d` পুনরায় চালান। ভেরিয়েবলের সম্পূর্ণ তালিকা [এখানে] (https://docs.dify.ai/getting-started/install-self-hosted/environments) খুঁজে পেতে পারেন।
|
||||
|
||||
যদি আপনি একটি হাইলি এভেইলেবল সেটআপ কনফিগার করতে চান, তাহলে কমিউনিটি [Helm Charts](https://helm.sh/) এবং YAML ফাইল রয়েছে যা Dify কে Kubernetes-এ ডিপ্লয় করার প্রক্রিয়া বর্ণনা করে।
|
||||
|
||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||
|
||||
#### টেরাফর্ম ব্যবহার করে ডিপ্লয়
|
||||
|
||||
[terraform](https://www.terraform.io/) ব্যবহার করে এক ক্লিকেই ক্লাউড প্ল্যাটফর্মে Dify ডিপ্লয় করুন।
|
||||
|
||||
##### অ্যাজুর গ্লোবাল
|
||||
|
||||
- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform)
|
||||
|
||||
##### গুগল ক্লাউড
|
||||
|
||||
- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||
|
||||
#### AWS CDK ব্যবহার করে ডিপ্লয়
|
||||
|
||||
[CDK](https://aws.amazon.com/cdk/) দিয়ে AWS-এ Dify ডিপ্লয় করুন
|
||||
|
||||
##### AWS
|
||||
|
||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
|
||||
## Contributing
|
||||
|
||||
যারা কোড অবদান রাখতে চান, তাদের জন্য আমাদের [অবদান নির্দেশিকা] দেখুন (https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)।
|
||||
একই সাথে, সোশ্যাল মিডিয়া এবং ইভেন্ট এবং কনফারেন্সে এটি শেয়ার করে Dify কে সমর্থন করুন।
|
||||
|
||||
> আমরা ম্যান্ডারিন বা ইংরেজি ছাড়া অন্য ভাষায় Dify অনুবাদ করতে সাহায্য করার জন্য অবদানকারীদের খুঁজছি। আপনি যদি সাহায্য করতে আগ্রহী হন, তাহলে আরও তথ্যের জন্য [i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) দেখুন এবং আমাদের [ডিসকর্ড কমিউনিটি সার্ভার](https://discord.gg/8Tpq4AcN9c) এর `গ্লোবাল-ইউজারস` চ্যানেলে আমাদের একটি মন্তব্য করুন।
|
||||
|
||||
## কমিউনিটি এবং যোগাযোগ
|
||||
|
||||
- [Github Discussion](https://github.com/langgenius/dify/discussions) ফিডব্যাক এবং প্রতিক্রিয়া জানানোর মাধ্যম।
|
||||
- [GitHub Issues](https://github.com/langgenius/dify/issues). Dify.AI ব্যবহার করে আপনি যেসব বাগের সম্মুখীন হন এবং ফিচার প্রস্তাবনা। আমাদের [অবদান নির্দেশিকা](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) দেখুন।
|
||||
- [Discord](https://discord.gg/FngNHpbcY7) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম।
|
||||
- [X(Twitter)](https://twitter.com/dify_ai) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম।
|
||||
|
||||
**অবদানকারীদের তালিকা**
|
||||
|
||||
<a href="https://github.com/langgenius/dify/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langgenius/dify" />
|
||||
</a>
|
||||
|
||||
## স্টার হিস্ট্রি
|
||||
|
||||
[](https://star-history.com/#langgenius/dify&Date)
|
||||
|
||||
## নিরাপত্তা বিষয়ক
|
||||
|
||||
আপনার গোপনীয়তা রক্ষা করতে, অনুগ্রহ করে GitHub-এ নিরাপত্তা সংক্রান্ত সমস্যা পোস্ট করা এড়িয়ে চলুন। পরিবর্তে, আপনার প্রশ্নগুলি <security@dify.ai> ঠিকানায় পাঠান এবং আমরা আপনাকে আরও বিস্তারিত উত্তর প্রদান করব।
|
||||
|
||||
## লাইসেন্স
|
||||
|
||||
এই রিপোজিটরিটি [ডিফাই ওপেন সোর্স লাইসেন্স](LICENSE) এর অধিনে , যা মূলত অ্যাপাচি ২.০, তবে কিছু অতিরিক্ত বিধিনিষেধ রয়েছে।
|
||||
@ -45,6 +45,7 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
259
README_DE.md
Normal file
259
README_DE.md
Normal file
@ -0,0 +1,259 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Einführung in Dify Workflow File Upload: Google NotebookLM Podcast nachbilden</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud.dify.ai">Dify Cloud</a> ·
|
||||
<a href="https://docs.dify.ai/getting-started/install-self-hosted">Selbstgehostetes</a> ·
|
||||
<a href="https://docs.dify.ai">Dokumentation</a> ·
|
||||
<a href="https://udify.app/chat/22L1zSxg6yW1cWQg">Anfrage an Unternehmen</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://dify.ai" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Product-F04438"></a>
|
||||
<a href="https://dify.ai/pricing" target="_blank">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/free-pricing?logo=free&color=%20%23155EEF&label=pricing&labelColor=%20%23528bff"></a>
|
||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||
alt="chat on Discord"></a>
|
||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
||||
alt="join Reddit"></a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||
alt="follow on X(Twitter)"></a>
|
||||
<a href="https://www.linkedin.com/company/langgenius/" target="_blank">
|
||||
<img src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"
|
||||
alt="follow on LinkedIn"></a>
|
||||
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langgenius/dify-web?labelColor=%20%23FDB062&color=%20%23f79009"></a>
|
||||
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||
<img alt="Commits last month" src="https://img.shields.io/github/commit-activity/m/langgenius/dify?labelColor=%20%2332b583&color=%20%2312b76a"></a>
|
||||
<a href="https://github.com/langgenius/dify/" target="_blank">
|
||||
<img alt="Issues closed" src="https://img.shields.io/github/issues-search?query=repo%3Alanggenius%2Fdify%20is%3Aclosed&label=issues%20closed&labelColor=%20%237d89b0&color=%20%235d6b98"></a>
|
||||
<a href="https://github.com/langgenius/dify/discussions/" target="_blank">
|
||||
<img alt="Discussion posts" src="https://img.shields.io/github/discussions/langgenius/dify?labelColor=%20%239b8afb&color=%20%237a5af8"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md"><img alt="README in English" src="https://img.shields.io/badge/English-d9d9d9"></a>
|
||||
<a href="./README_CN.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
|
||||
<a href="./README_JA.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
|
||||
<a href="./README_ES.md"><img alt="README en Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
|
||||
<a href="./README_FR.md"><img alt="README en Français" src="https://img.shields.io/badge/Français-d9d9d9"></a>
|
||||
<a href="./README_KL.md"><img alt="README tlhIngan Hol" src="https://img.shields.io/badge/Klingon-d9d9d9"></a>
|
||||
<a href="./README_KR.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_DE.md"><img alt="README in Deutsch" src="https://img.shields.io/badge/German-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
Dify ist eine Open-Source-Plattform zur Entwicklung von LLM-Anwendungen. Ihre intuitive Benutzeroberfläche vereint agentenbasierte KI-Workflows, RAG-Pipelines, Agentenfunktionen, Modellverwaltung, Überwachungsfunktionen und mehr, sodass Sie schnell von einem Prototyp in die Produktion übergehen können.
|
||||
|
||||
## Schnellstart
|
||||
> Bevor Sie Dify installieren, stellen Sie sicher, dass Ihr System die folgenden Mindestanforderungen erfüllt:
|
||||
>
|
||||
>- CPU >= 2 Core
|
||||
>- RAM >= 4 GiB
|
||||
|
||||
</br>
|
||||
|
||||
Der einfachste Weg, den Dify-Server zu starten, ist über [docker compose](docker/docker-compose.yaml). Stellen Sie vor dem Ausführen von Dify mit den folgenden Befehlen sicher, dass [Docker](https://docs.docker.com/get-docker/) und [Docker Compose](https://docs.docker.com/compose/install/) auf Ihrem System installiert sind:
|
||||
|
||||
```bash
|
||||
cd dify
|
||||
cd docker
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Nachdem Sie den Server gestartet haben, können Sie über Ihren Browser auf das Dify Dashboard unter [http://localhost/install](http://localhost/install) zugreifen und den Initialisierungsprozess starten.
|
||||
|
||||
#### Hilfe suchen
|
||||
Bitte beachten Sie unsere [FAQ](https://docs.dify.ai/getting-started/install-self-hosted/faqs), wenn Sie Probleme bei der Einrichtung von Dify haben. Wenden Sie sich an [die Community und uns](#community--contact), falls weiterhin Schwierigkeiten auftreten.
|
||||
|
||||
> Wenn Sie zu Dify beitragen oder zusätzliche Entwicklungen durchführen möchten, lesen Sie bitte unseren [Leitfaden zur Bereitstellung aus dem Quellcode](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code).
|
||||
|
||||
## Wesentliche Merkmale
|
||||
**1. Workflow**:
|
||||
Erstellen und testen Sie leistungsstarke KI-Workflows auf einer visuellen Oberfläche, wobei Sie alle der folgenden Funktionen und darüber hinaus nutzen können.
|
||||
|
||||
|
||||
https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa
|
||||
|
||||
|
||||
|
||||
**2. Umfassende Modellunterstützung**:
|
||||
Nahtlose Integration mit Hunderten von proprietären und Open-Source-LLMs von Dutzenden Inferenzanbietern und selbstgehosteten Lösungen, die GPT, Mistral, Llama3 und alle mit der OpenAI API kompatiblen Modelle abdecken. Eine vollständige Liste der unterstützten Modellanbieter finden Sie [hier](https://docs.dify.ai/getting-started/readme/model-providers).
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
**3. Prompt IDE**:
|
||||
Intuitive Benutzeroberfläche zum Erstellen von Prompts, zum Vergleichen der Modellleistung und zum Hinzufügen zusätzlicher Funktionen wie Text-to-Speech in einer chatbasierten Anwendung.
|
||||
|
||||
**4. RAG Pipeline**:
|
||||
Umfassende RAG-Funktionalitäten, die alles von der Dokumenteneinlesung bis zur -abfrage abdecken, mit sofort einsatzbereiter Unterstützung für die Textextraktion aus PDFs, PPTs und anderen gängigen Dokumentformaten.
|
||||
|
||||
**5. Fähigkeiten des Agenten**:
|
||||
Sie können Agenten basierend auf LLM Function Calling oder ReAct definieren und vorgefertigte oder benutzerdefinierte Tools für den Agenten hinzufügen. Dify stellt über 50 integrierte Tools für KI-Agenten bereit, wie zum Beispiel Google Search, DALL·E, Stable Diffusion und WolframAlpha.
|
||||
|
||||
**6. LLMOps**:
|
||||
Überwachen und analysieren Sie Anwendungsprotokolle und die Leistung im Laufe der Zeit. Sie können kontinuierlich Prompts, Datensätze und Modelle basierend auf Produktionsdaten und Annotationen verbessern.
|
||||
|
||||
**7. Backend-as-a-Service**:
|
||||
Alle Dify-Angebote kommen mit entsprechenden APIs, sodass Sie Dify mühelos in Ihre eigene Geschäftslogik integrieren können.
|
||||
|
||||
## Vergleich der Merkmale
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<th align="center">Feature</th>
|
||||
<th align="center">Dify.AI</th>
|
||||
<th align="center">LangChain</th>
|
||||
<th align="center">Flowise</th>
|
||||
<th align="center">OpenAI Assistants API</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Programming Approach</td>
|
||||
<td align="center">API + App-oriented</td>
|
||||
<td align="center">Python Code</td>
|
||||
<td align="center">App-oriented</td>
|
||||
<td align="center">API-oriented</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Supported LLMs</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
<td align="center">Rich Variety</td>
|
||||
<td align="center">OpenAI-only</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">RAG Engine</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Agent</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Workflow</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Observability</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Enterprise Feature (SSO/Access control)</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">❌</td>
|
||||
<td align="center">❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Local Deployment</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">✅</td>
|
||||
<td align="center">❌</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Dify verwenden
|
||||
|
||||
- **Cloud </br>**
|
||||
Wir hosten einen [Dify Cloud](https://dify.ai)-Service, den jeder ohne Einrichtung ausprobieren kann. Er bietet alle Funktionen der selbstgehosteten Version und beinhaltet 200 kostenlose GPT-4-Aufrufe im Sandbox-Plan.
|
||||
|
||||
- **Selbstgehostete Dify Community Edition</br>**
|
||||
Starten Sie Dify schnell in Ihrer Umgebung mit diesem [Schnellstart-Leitfaden](#quick-start). Nutzen Sie unsere [Dokumentation](https://docs.dify.ai) für weiterführende Informationen und detaillierte Anweisungen.
|
||||
|
||||
- **Dify für Unternehmen / Organisationen</br>**
|
||||
Wir bieten zusätzliche, unternehmensspezifische Funktionen. [Über diesen Chatbot können Sie uns Ihre Fragen mitteilen](https://udify.app/chat/22L1zSxg6yW1cWQg) oder [senden Sie uns eine E-Mail](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry), um Ihre unternehmerischen Bedürfnisse zu besprechen. </br>
|
||||
> Für Startups und kleine Unternehmen, die AWS nutzen, schauen Sie sich [Dify Premium on AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) an und stellen Sie es mit nur einem Klick in Ihrer eigenen AWS VPC bereit. Es handelt sich um ein erschwingliches AMI-Angebot mit der Option, Apps mit individuellem Logo und Branding zu erstellen.
|
||||
|
||||
|
||||
## Immer einen Schritt voraus
|
||||
|
||||
Star Dify auf GitHub und lassen Sie sich sofort über neue Releases benachrichtigen.
|
||||
|
||||

|
||||
|
||||
|
||||
## Erweiterte Einstellungen
|
||||
|
||||
Falls Sie die Konfiguration anpassen müssen, lesen Sie bitte die Kommentare in unserer [.env.example](docker/.env.example)-Datei und aktualisieren Sie die entsprechenden Werte in Ihrer `.env`-Datei. Zusätzlich müssen Sie eventuell Anpassungen an der `docker-compose.yaml`-Datei vornehmen, wie zum Beispiel das Ändern von Image-Versionen, Portzuordnungen oder Volumen-Mounts, je nach Ihrer spezifischen Einsatzumgebung und Ihren Anforderungen. Nachdem Sie Änderungen vorgenommen haben, starten Sie `docker-compose up -d` erneut. Eine vollständige Liste der verfügbaren Umgebungsvariablen finden Sie [hier](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
||||
|
||||
Falls Sie eine hochverfügbare Konfiguration einrichten möchten, gibt es von der Community bereitgestellte [Helm Charts](https://helm.sh/) und YAML-Dateien, die es ermöglichen, Dify auf Kubernetes bereitzustellen.
|
||||
|
||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||
|
||||
#### Terraform für die Bereitstellung verwenden
|
||||
|
||||
Stellen Sie Dify mit nur einem Klick mithilfe von [terraform](https://www.terraform.io/) auf einer Cloud-Plattform bereit.
|
||||
|
||||
##### Azure Global
|
||||
- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform)
|
||||
|
||||
##### Google Cloud
|
||||
- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||
|
||||
#### Verwendung von AWS CDK für die Bereitstellung
|
||||
|
||||
Bereitstellung von Dify auf AWS mit [CDK](https://aws.amazon.com/cdk/)
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
|
||||
## Contributing
|
||||
|
||||
Falls Sie Code beitragen möchten, lesen Sie bitte unseren [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). Gleichzeitig bitten wir Sie, Dify zu unterstützen, indem Sie es in den sozialen Medien teilen und auf Veranstaltungen und Konferenzen präsentieren.
|
||||
|
||||
|
||||
> Wir suchen Mitwirkende, die dabei helfen, Dify in weitere Sprachen zu übersetzen – außer Mandarin oder Englisch. Wenn Sie Interesse an einer Mitarbeit haben, lesen Sie bitte die [i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) für weitere Informationen und hinterlassen Sie einen Kommentar im `global-users`-Kanal unseres [Discord Community Servers](https://discord.gg/8Tpq4AcN9c).
|
||||
|
||||
## Gemeinschaft & Kontakt
|
||||
|
||||
* [Github Discussion](https://github.com/langgenius/dify/discussions). Am besten geeignet für: den Austausch von Feedback und das Stellen von Fragen.
|
||||
* [GitHub Issues](https://github.com/langgenius/dify/issues). Am besten für: Fehler, auf die Sie bei der Verwendung von Dify.AI stoßen, und Funktionsvorschläge. Siehe unseren [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||
* [Discord](https://discord.gg/FngNHpbcY7). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community.
|
||||
* [X(Twitter)](https://twitter.com/dify_ai). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community.
|
||||
|
||||
**Mitwirkende**
|
||||
|
||||
<a href="https://github.com/langgenius/dify/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langgenius/dify" />
|
||||
</a>
|
||||
|
||||
## Star-Geschichte
|
||||
|
||||
[](https://star-history.com/#langgenius/dify&Date)
|
||||
|
||||
|
||||
## Offenlegung der Sicherheit
|
||||
|
||||
Um Ihre Privatsphäre zu schützen, vermeiden Sie es bitte, Sicherheitsprobleme auf GitHub zu posten. Schicken Sie Ihre Fragen stattdessen an security@dify.ai und wir werden Ihnen eine ausführlichere Antwort geben.
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Repository steht unter der [Dify Open Source License](LICENSE), die im Wesentlichen Apache 2.0 mit einigen zusätzlichen Einschränkungen ist.
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
#
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
#
|
||||
|
||||
23
README_JA.md
23
README_JA.md
@ -15,7 +15,7 @@
|
||||
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||
alt="Discordでチャット"></a>
|
||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
||||
<a href="https://reddit.com/r/difyai" target="_blank">
|
||||
<img src="https://img.shields.io/reddit/subreddit-subscribers/difyai?style=plastic&logo=reddit&label=r%2Fdifyai&labelColor=white"
|
||||
alt="Reddit"></a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||
@ -45,6 +45,7 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
#
|
||||
@ -56,7 +57,7 @@
|
||||
DifyはオープンソースのLLMアプリケーション開発プラットフォームです。直感的なインターフェイスには、AIワークフロー、RAGパイプライン、エージェント機能、モデル管理、観測機能などが組み合わさっており、プロトタイプから生産まで迅速に進めることができます。以下の機能が含まれます:
|
||||
</br> </br>
|
||||
|
||||
**1. ワークフロー**:
|
||||
**1. ワークフロー**:
|
||||
強力なAIワークフローをビジュアルキャンバス上で構築し、テストできます。すべての機能、および以下の機能を使用できます。
|
||||
|
||||
|
||||
@ -64,25 +65,25 @@ DifyはオープンソースのLLMアプリケーション開発プラットフ
|
||||
|
||||
|
||||
|
||||
**2. 総合的なモデルサポート**:
|
||||
**2. 総合的なモデルサポート**:
|
||||
数百ものプロプライエタリ/オープンソースのLLMと、数十もの推論プロバイダーおよびセルフホスティングソリューションとのシームレスな統合を提供します。GPT、Mistral、Llama3、OpenAI APIと互換性のあるすべてのモデルを統合されています。サポートされているモデルプロバイダーの完全なリストは[こちら](https://docs.dify.ai/getting-started/readme/model-providers)をご覧ください。
|
||||
|
||||

|
||||
|
||||
|
||||
**3. プロンプトIDE**:
|
||||
**3. プロンプトIDE**:
|
||||
プロンプトの作成、モデルパフォーマンスの比較が行え、チャットベースのアプリに音声合成などの機能も追加できます。
|
||||
|
||||
**4. RAGパイプライン**:
|
||||
**4. RAGパイプライン**:
|
||||
ドキュメントの取り込みから検索までをカバーする広範なRAG機能ができます。ほかにもPDF、PPT、その他の一般的なドキュメントフォーマットからのテキスト抽出のサポートも提供します。
|
||||
|
||||
**5. エージェント機能**:
|
||||
**5. エージェント機能**:
|
||||
LLM Function CallingやReActに基づくエージェントの定義が可能で、AIエージェント用のプリビルトまたはカスタムツールを追加できます。Difyには、Google検索、DALL·E、Stable Diffusion、WolframAlphaなどのAIエージェント用の50以上の組み込みツールが提供します。
|
||||
|
||||
**6. LLMOps**:
|
||||
**6. LLMOps**:
|
||||
アプリケーションのログやパフォーマンスを監視と分析し、生産のデータと注釈に基づいて、プロンプト、データセット、モデルを継続的に改善できます。
|
||||
|
||||
**7. Backend-as-a-Service**:
|
||||
**7. Backend-as-a-Service**:
|
||||
すべての機能はAPIを提供されており、Difyを自分のビジネスロジックに簡単に統合できます。
|
||||
|
||||
|
||||
@ -164,7 +165,7 @@ DifyはオープンソースのLLMアプリケーション開発プラットフ
|
||||
|
||||
- **企業/組織向けのDify</br>**
|
||||
企業中心の機能を提供しています。[メールを送信](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry)して企業のニーズについて相談してください。 </br>
|
||||
> AWSを使用しているスタートアップ企業や中小企業の場合は、[AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t23mebxzwjhu6)のDify Premiumをチェックして、ワンクリックで自分のAWS VPCにデプロイできます。さらに、手頃な価格のAMIオファリングとして、ロゴやブランディングをカスタマイズしてアプリケーションを作成するオプションがあります。
|
||||
> AWSを使用しているスタートアップ企業や中小企業の場合は、[AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6)のDify Premiumをチェックして、ワンクリックで自分のAWS VPCにデプロイできます。さらに、手頃な価格のAMIオファリングとして、ロゴやブランディングをカスタマイズしてアプリケーションを作成するオプションがあります。
|
||||
|
||||
|
||||
## 最新の情報を入手
|
||||
@ -177,7 +178,7 @@ GitHub上でDifyにスターを付けることで、Difyに関する新しいニ
|
||||
|
||||
## クイックスタート
|
||||
> Difyをインストールする前に、お使いのマシンが以下の最小システム要件を満たしていることを確認してください:
|
||||
>
|
||||
>
|
||||
>- CPU >= 2コア
|
||||
>- RAM >= 4GB
|
||||
|
||||
@ -219,7 +220,7 @@ docker compose up -d
|
||||
|
||||
[CDK](https://aws.amazon.com/cdk/) を使用して、DifyをAWSにデプロイします
|
||||
|
||||
##### AWS
|
||||
##### AWS
|
||||
- [@KevinZhaoによるAWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
|
||||
## 貢献
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
#
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@ -50,6 +50,7 @@
|
||||
<a href="./README_TR.md"><img alt="README em Turco" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README em Vietnamita" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_PT.md"><img alt="README em Português - BR" src="https://img.shields.io/badge/Portugu%C3%AAs-BR?style=flat&label=BR&color=d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
Dify é uma plataforma de desenvolvimento de aplicativos LLM de código aberto. Sua interface intuitiva combina workflow de IA, pipeline RAG, capacidades de agente, gerenciamento de modelos, recursos de observabilidade e muito mais, permitindo que você vá rapidamente do protótipo à produção. Aqui está uma lista das principais funcionalidades:
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_SI.md"><img alt="README Slovenščina" src="https://img.shields.io/badge/Sloven%C5%A1%C4%8Dina-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
<a href="./README_AR.md"><img alt="README بالعربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||
<a href="./README_TR.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||
<a href="./README_VI.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||
<a href="./README_BN.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@ -427,7 +427,6 @@ PLUGIN_DAEMON_URL=http://127.0.0.1:5002
|
||||
PLUGIN_REMOTE_INSTALL_PORT=5003
|
||||
PLUGIN_REMOTE_INSTALL_HOST=localhost
|
||||
PLUGIN_MAX_PACKAGE_SIZE=15728640
|
||||
INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||
INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||
|
||||
# Marketplace configuration
|
||||
|
||||
@ -7,7 +7,7 @@ line-length = 120
|
||||
quote-style = "double"
|
||||
|
||||
[lint]
|
||||
preview = true
|
||||
preview = false
|
||||
select = [
|
||||
"B", # flake8-bugbear rules
|
||||
"C4", # flake8-comprehensions
|
||||
@ -18,7 +18,6 @@ select = [
|
||||
"N", # pep8-naming
|
||||
"PT", # flake8-pytest-style rules
|
||||
"PLC0208", # iteration-over-set
|
||||
"PLC2801", # unnecessary-dunder-call
|
||||
"PLC0414", # useless-import-alias
|
||||
"PLE0604", # invalid-all-object
|
||||
"PLE0605", # invalid-all-format
|
||||
@ -46,7 +45,6 @@ ignore = [
|
||||
"E712", # true-false-comparison
|
||||
"E721", # type-comparison
|
||||
"E722", # bare-except
|
||||
"E731", # lambda-assignment
|
||||
"F821", # undefined-name
|
||||
"F841", # unused-variable
|
||||
"FURB113", # repeated-append
|
||||
|
||||
@ -389,11 +389,6 @@ class InnerAPIConfig(BaseSettings):
|
||||
default=False,
|
||||
)
|
||||
|
||||
INNER_API_KEY: Optional[str] = Field(
|
||||
description="API key for accessing the internal API",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
class LoggingConfig(BaseSettings):
|
||||
"""
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field, PositiveInt
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
@ -9,16 +9,6 @@ class OracleConfig(BaseSettings):
|
||||
Configuration settings for Oracle database
|
||||
"""
|
||||
|
||||
ORACLE_HOST: Optional[str] = Field(
|
||||
description="Hostname or IP address of the Oracle database server (e.g., 'localhost' or 'oracle.example.com')",
|
||||
default=None,
|
||||
)
|
||||
|
||||
ORACLE_PORT: PositiveInt = Field(
|
||||
description="Port number on which the Oracle database server is listening (default is 1521)",
|
||||
default=1521,
|
||||
)
|
||||
|
||||
ORACLE_USER: Optional[str] = Field(
|
||||
description="Username for authenticating with the Oracle database",
|
||||
default=None,
|
||||
@ -29,7 +19,28 @@ class OracleConfig(BaseSettings):
|
||||
default=None,
|
||||
)
|
||||
|
||||
ORACLE_DATABASE: Optional[str] = Field(
|
||||
description="Name of the Oracle database or service to connect to (e.g., 'ORCL' or 'pdborcl')",
|
||||
ORACLE_DSN: Optional[str] = Field(
|
||||
description="Oracle database connection string. For traditional database, use format 'host:port/service_name'. "
|
||||
"For autonomous database, use the service name from tnsnames.ora in the wallet",
|
||||
default=None,
|
||||
)
|
||||
|
||||
ORACLE_CONFIG_DIR: Optional[str] = Field(
|
||||
description="Directory containing the tnsnames.ora configuration file. Only used in thin mode connection",
|
||||
default=None,
|
||||
)
|
||||
|
||||
ORACLE_WALLET_LOCATION: Optional[str] = Field(
|
||||
description="Oracle wallet directory path containing the wallet files for secure connection",
|
||||
default=None,
|
||||
)
|
||||
|
||||
ORACLE_WALLET_PASSWORD: Optional[str] = Field(
|
||||
description="Password to decrypt the Oracle wallet, if it is encrypted",
|
||||
default=None,
|
||||
)
|
||||
|
||||
ORACLE_IS_AUTONOMOUS: bool = Field(
|
||||
description="Flag indicating whether connecting to Oracle Autonomous Database",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
|
||||
from contexts.wrapper import RecyclableContextVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.model_runtime.entities.model_entities import AIModelEntity
|
||||
from core.plugin.entities.plugin_daemon import PluginModelProviderEntity
|
||||
from core.tools.plugin_tool.provider import PluginToolProviderController
|
||||
from core.workflow.entities.variable_pool import VariablePool
|
||||
@ -20,11 +21,19 @@ To avoid race-conditions caused by gunicorn thread recycling, using RecyclableCo
|
||||
plugin_tool_providers: RecyclableContextVar[dict[str, "PluginToolProviderController"]] = RecyclableContextVar(
|
||||
ContextVar("plugin_tool_providers")
|
||||
)
|
||||
|
||||
plugin_tool_providers_lock: RecyclableContextVar[Lock] = RecyclableContextVar(ContextVar("plugin_tool_providers_lock"))
|
||||
|
||||
plugin_model_providers: RecyclableContextVar[list["PluginModelProviderEntity"] | None] = RecyclableContextVar(
|
||||
ContextVar("plugin_model_providers")
|
||||
)
|
||||
|
||||
plugin_model_providers_lock: RecyclableContextVar[Lock] = RecyclableContextVar(
|
||||
ContextVar("plugin_model_providers_lock")
|
||||
)
|
||||
|
||||
plugin_model_schema_lock: RecyclableContextVar[Lock] = RecyclableContextVar(ContextVar("plugin_model_schema_lock"))
|
||||
|
||||
plugin_model_schemas: RecyclableContextVar[dict[str, "AIModelEntity"]] = RecyclableContextVar(
|
||||
ContextVar("plugin_model_schemas")
|
||||
)
|
||||
|
||||
@ -2,7 +2,6 @@ from datetime import UTC, datetime
|
||||
|
||||
from flask_login import current_user # type: ignore
|
||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
from constants.languages import supported_language
|
||||
@ -51,37 +50,35 @@ class AppSite(Resource):
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
|
||||
with Session(db.engine) as session:
|
||||
site = session.query(Site).filter(Site.app_id == app_model.id).first()
|
||||
site = db.session.query(Site).filter(Site.app_id == app_model.id).first()
|
||||
if not site:
|
||||
raise NotFound
|
||||
|
||||
if not site:
|
||||
raise NotFound
|
||||
for attr_name in [
|
||||
"title",
|
||||
"icon_type",
|
||||
"icon",
|
||||
"icon_background",
|
||||
"description",
|
||||
"default_language",
|
||||
"chat_color_theme",
|
||||
"chat_color_theme_inverted",
|
||||
"customize_domain",
|
||||
"copyright",
|
||||
"privacy_policy",
|
||||
"custom_disclaimer",
|
||||
"customize_token_strategy",
|
||||
"prompt_public",
|
||||
"show_workflow_steps",
|
||||
"use_icon_as_answer_icon",
|
||||
]:
|
||||
value = args.get(attr_name)
|
||||
if value is not None:
|
||||
setattr(site, attr_name, value)
|
||||
|
||||
for attr_name in [
|
||||
"title",
|
||||
"icon_type",
|
||||
"icon",
|
||||
"icon_background",
|
||||
"description",
|
||||
"default_language",
|
||||
"chat_color_theme",
|
||||
"chat_color_theme_inverted",
|
||||
"customize_domain",
|
||||
"copyright",
|
||||
"privacy_policy",
|
||||
"custom_disclaimer",
|
||||
"customize_token_strategy",
|
||||
"prompt_public",
|
||||
"show_workflow_steps",
|
||||
"use_icon_as_answer_icon",
|
||||
]:
|
||||
value = args.get(attr_name)
|
||||
if value is not None:
|
||||
setattr(site, attr_name, value)
|
||||
|
||||
site.updated_by = current_user.id
|
||||
site.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
||||
session.commit()
|
||||
site.updated_by = current_user.id
|
||||
site.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
||||
db.session.commit()
|
||||
|
||||
return site
|
||||
|
||||
|
||||
@ -582,6 +582,15 @@ class AdvancedChatAppGenerateTaskPipeline:
|
||||
session.commit()
|
||||
|
||||
yield workflow_finish_resp
|
||||
elif event.stopped_by in (
|
||||
QueueStopEvent.StopBy.INPUT_MODERATION,
|
||||
QueueStopEvent.StopBy.ANNOTATION_REPLY,
|
||||
):
|
||||
# When hitting input-moderation or annotation-reply, the workflow will not start
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
# Save message
|
||||
self._save_message(session=session)
|
||||
session.commit()
|
||||
|
||||
yield self._message_end_to_stream_response()
|
||||
break
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import decimal
|
||||
import hashlib
|
||||
from threading import Lock
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
import contexts
|
||||
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 (
|
||||
@ -139,15 +142,35 @@ class AIModel(BaseModel):
|
||||
:return: model schema
|
||||
"""
|
||||
plugin_model_manager = PluginModelManager()
|
||||
return plugin_model_manager.get_model_schema(
|
||||
tenant_id=self.tenant_id,
|
||||
user_id="unknown",
|
||||
plugin_id=self.plugin_id,
|
||||
provider=self.provider_name,
|
||||
model_type=self.model_type.value,
|
||||
model=model,
|
||||
credentials=credentials or {},
|
||||
)
|
||||
cache_key = f"{self.tenant_id}:{self.plugin_id}:{self.provider_name}:{self.model_type.value}:{model}"
|
||||
# sort credentials
|
||||
sorted_credentials = sorted(credentials.items()) if credentials else []
|
||||
cache_key += ":".join([hashlib.md5(f"{k}:{v}".encode()).hexdigest() for k, v in sorted_credentials])
|
||||
|
||||
try:
|
||||
contexts.plugin_model_schemas.get()
|
||||
except LookupError:
|
||||
contexts.plugin_model_schemas.set({})
|
||||
contexts.plugin_model_schema_lock.set(Lock())
|
||||
|
||||
with contexts.plugin_model_schema_lock.get():
|
||||
if cache_key in contexts.plugin_model_schemas.get():
|
||||
return contexts.plugin_model_schemas.get()[cache_key]
|
||||
|
||||
schema = plugin_model_manager.get_model_schema(
|
||||
tenant_id=self.tenant_id,
|
||||
user_id="unknown",
|
||||
plugin_id=self.plugin_id,
|
||||
provider=self.provider_name,
|
||||
model_type=self.model_type.value,
|
||||
model=model,
|
||||
credentials=credentials or {},
|
||||
)
|
||||
|
||||
if schema:
|
||||
contexts.plugin_model_schemas.get()[cache_key] = schema
|
||||
|
||||
return schema
|
||||
|
||||
def get_customizable_model_schema_from_credentials(self, model: str, credentials: dict) -> Optional[AIModelEntity]:
|
||||
"""
|
||||
@ -157,14 +180,9 @@ class AIModel(BaseModel):
|
||||
: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
|
||||
"""
|
||||
# get customizable model schema
|
||||
schema = self.get_customizable_model_schema(model, credentials)
|
||||
|
||||
if not schema:
|
||||
return None
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
@ -206,17 +207,35 @@ class ModelProviderFactory:
|
||||
Get model schema
|
||||
"""
|
||||
plugin_id, provider_name = self.get_plugin_id_and_provider_name_from_provider(provider)
|
||||
model_schema = self.plugin_model_manager.get_model_schema(
|
||||
tenant_id=self.tenant_id,
|
||||
user_id="unknown",
|
||||
plugin_id=plugin_id,
|
||||
provider=provider_name,
|
||||
model_type=model_type.value,
|
||||
model=model,
|
||||
credentials=credentials,
|
||||
)
|
||||
cache_key = f"{self.tenant_id}:{plugin_id}:{provider_name}:{model_type.value}:{model}"
|
||||
# sort credentials
|
||||
sorted_credentials = sorted(credentials.items()) if credentials else []
|
||||
cache_key += ":".join([hashlib.md5(f"{k}:{v}".encode()).hexdigest() for k, v in sorted_credentials])
|
||||
|
||||
return model_schema
|
||||
try:
|
||||
contexts.plugin_model_schemas.get()
|
||||
except LookupError:
|
||||
contexts.plugin_model_schemas.set({})
|
||||
contexts.plugin_model_schema_lock.set(Lock())
|
||||
|
||||
with contexts.plugin_model_schema_lock.get():
|
||||
if cache_key in contexts.plugin_model_schemas.get():
|
||||
return contexts.plugin_model_schemas.get()[cache_key]
|
||||
|
||||
schema = self.plugin_model_manager.get_model_schema(
|
||||
tenant_id=self.tenant_id,
|
||||
user_id="unknown",
|
||||
plugin_id=plugin_id,
|
||||
provider=provider_name,
|
||||
model_type=model_type.value,
|
||||
model=model,
|
||||
credentials=credentials or {},
|
||||
)
|
||||
|
||||
if schema:
|
||||
contexts.plugin_model_schemas.get()[cache_key] = schema
|
||||
|
||||
return schema
|
||||
|
||||
def get_models(
|
||||
self,
|
||||
|
||||
@ -72,8 +72,18 @@ class MilvusVector(BaseVector):
|
||||
self._client = self._init_client(config)
|
||||
self._consistency_level = "Session" # Consistency level for Milvus operations
|
||||
self._fields: list[str] = [] # List of fields in the collection
|
||||
if self._client.has_collection(collection_name):
|
||||
self._load_collection_fields()
|
||||
self._hybrid_search_enabled = self._check_hybrid_search_support() # Check if hybrid search is supported
|
||||
|
||||
def _load_collection_fields(self, fields: Optional[list[str]] = None) -> None:
|
||||
if fields is None:
|
||||
# Load collection fields from remote server
|
||||
collection_info = self._client.describe_collection(self._collection_name)
|
||||
fields = [field["name"] for field in collection_info["fields"]]
|
||||
# Since primary field is auto-id, no need to track it
|
||||
self._fields = [f for f in fields if f != Field.PRIMARY_KEY.value]
|
||||
|
||||
def _check_hybrid_search_support(self) -> bool:
|
||||
"""
|
||||
Check if the current Milvus version supports hybrid search.
|
||||
@ -306,10 +316,7 @@ class MilvusVector(BaseVector):
|
||||
)
|
||||
schema.add_function(bm25_function)
|
||||
|
||||
for x in schema.fields:
|
||||
self._fields.append(x.name)
|
||||
# Since primary field is auto-id, no need to track it
|
||||
self._fields.remove(Field.PRIMARY_KEY.value)
|
||||
self._load_collection_fields([f.name for f in schema.fields])
|
||||
|
||||
# Create Index params for the collection
|
||||
index_params_obj = IndexParams()
|
||||
|
||||
@ -23,25 +23,30 @@ oracledb.defaults.fetch_lobs = False
|
||||
|
||||
|
||||
class OracleVectorConfig(BaseModel):
|
||||
host: str
|
||||
port: int
|
||||
user: str
|
||||
password: str
|
||||
database: str
|
||||
dsn: str
|
||||
config_dir: str | None = None
|
||||
wallet_location: str | None = None
|
||||
wallet_password: str | None = None
|
||||
is_autonomous: bool = False
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_config(cls, values: dict) -> dict:
|
||||
if not values["host"]:
|
||||
raise ValueError("config ORACLE_HOST is required")
|
||||
if not values["port"]:
|
||||
raise ValueError("config ORACLE_PORT is required")
|
||||
if not values["user"]:
|
||||
raise ValueError("config ORACLE_USER is required")
|
||||
if not values["password"]:
|
||||
raise ValueError("config ORACLE_PASSWORD is required")
|
||||
if not values["database"]:
|
||||
raise ValueError("config ORACLE_DB is required")
|
||||
if not values["dsn"]:
|
||||
raise ValueError("config ORACLE_DSN is required")
|
||||
if values.get("is_autonomous", False):
|
||||
if not values.get("config_dir"):
|
||||
raise ValueError("config_dir is required for autonomous database")
|
||||
if not values.get("wallet_location"):
|
||||
raise ValueError("wallet_location is required for autonomous database")
|
||||
if not values.get("wallet_password"):
|
||||
raise ValueError("wallet_password is required for autonomous database")
|
||||
return values
|
||||
|
||||
|
||||
@ -56,7 +61,7 @@ CREATE TABLE IF NOT EXISTS {table_name} (
|
||||
SQL_CREATE_INDEX = """
|
||||
CREATE INDEX IF NOT EXISTS idx_docs_{table_name} ON {table_name}(text)
|
||||
INDEXTYPE IS CTXSYS.CONTEXT PARAMETERS
|
||||
('FILTER CTXSYS.NULL_FILTER SECTION GROUP CTXSYS.HTML_SECTION_GROUP LEXER sys.my_chinese_vgram_lexer')
|
||||
('FILTER CTXSYS.NULL_FILTER SECTION GROUP CTXSYS.HTML_SECTION_GROUP LEXER multilingual_lexer')
|
||||
"""
|
||||
|
||||
|
||||
@ -103,14 +108,25 @@ class OracleVector(BaseVector):
|
||||
)
|
||||
|
||||
def _create_connection_pool(self, config: OracleVectorConfig):
|
||||
return oracledb.create_pool(
|
||||
user=config.user,
|
||||
password=config.password,
|
||||
dsn="{}:{}/{}".format(config.host, config.port, config.database),
|
||||
min=1,
|
||||
max=50,
|
||||
increment=1,
|
||||
)
|
||||
pool_params = {
|
||||
"user": config.user,
|
||||
"password": config.password,
|
||||
"dsn": config.dsn,
|
||||
"min": 1,
|
||||
"max": 50,
|
||||
"increment": 1,
|
||||
}
|
||||
|
||||
if config.is_autonomous:
|
||||
pool_params.update(
|
||||
{
|
||||
"config_dir": config.config_dir,
|
||||
"wallet_location": config.wallet_location,
|
||||
"wallet_password": config.wallet_password,
|
||||
}
|
||||
)
|
||||
|
||||
return oracledb.create_pool(**pool_params)
|
||||
|
||||
@contextmanager
|
||||
def _get_cursor(self):
|
||||
@ -287,10 +303,12 @@ class OracleVectorFactory(AbstractVectorFactory):
|
||||
return OracleVector(
|
||||
collection_name=collection_name,
|
||||
config=OracleVectorConfig(
|
||||
host=dify_config.ORACLE_HOST or "localhost",
|
||||
port=dify_config.ORACLE_PORT,
|
||||
user=dify_config.ORACLE_USER or "system",
|
||||
password=dify_config.ORACLE_PASSWORD or "oracle",
|
||||
database=dify_config.ORACLE_DATABASE or "orcl",
|
||||
dsn=dify_config.ORACLE_DSN or "oracle:1521/freepdb1",
|
||||
config_dir=dify_config.ORACLE_CONFIG_DIR,
|
||||
wallet_location=dify_config.ORACLE_WALLET_LOCATION,
|
||||
wallet_password=dify_config.ORACLE_WALLET_PASSWORD,
|
||||
is_autonomous=dify_config.ORACLE_IS_AUTONOMOUS,
|
||||
),
|
||||
)
|
||||
|
||||
@ -172,7 +172,7 @@ class DatasetRetrieverTool(DatasetRetrieverBaseTool):
|
||||
"segment_id": segment.id,
|
||||
"retriever_from": self.retriever_from,
|
||||
"score": record.score or 0.0,
|
||||
"doc_metadata": document.doc_metadat, # type: ignore
|
||||
"doc_metadata": document.doc_metadata, # type: ignore
|
||||
}
|
||||
|
||||
if self.retriever_from == "dev":
|
||||
|
||||
@ -590,8 +590,6 @@ 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] = []
|
||||
|
||||
@ -191,6 +191,22 @@ class LLMNode(BaseNode[LLMNodeData]):
|
||||
# deduct quota
|
||||
self.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage)
|
||||
break
|
||||
outputs = {"text": result_text, "usage": jsonable_encoder(usage), "finish_reason": finish_reason}
|
||||
|
||||
yield RunCompletedEvent(
|
||||
run_result=NodeRunResult(
|
||||
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||
inputs=node_inputs,
|
||||
process_data=process_data,
|
||||
outputs=outputs,
|
||||
metadata={
|
||||
NodeRunMetadataKey.TOTAL_TOKENS: usage.total_tokens,
|
||||
NodeRunMetadataKey.TOTAL_PRICE: usage.total_price,
|
||||
NodeRunMetadataKey.CURRENCY: usage.currency,
|
||||
},
|
||||
llm_usage=usage,
|
||||
)
|
||||
)
|
||||
except LLMNodeError as e:
|
||||
yield RunCompletedEvent(
|
||||
run_result=NodeRunResult(
|
||||
@ -211,23 +227,6 @@ class LLMNode(BaseNode[LLMNodeData]):
|
||||
)
|
||||
)
|
||||
|
||||
outputs = {"text": result_text, "usage": jsonable_encoder(usage), "finish_reason": finish_reason}
|
||||
|
||||
yield RunCompletedEvent(
|
||||
run_result=NodeRunResult(
|
||||
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||
inputs=node_inputs,
|
||||
process_data=process_data,
|
||||
outputs=outputs,
|
||||
metadata={
|
||||
NodeRunMetadataKey.TOTAL_TOKENS: usage.total_tokens,
|
||||
NodeRunMetadataKey.TOTAL_PRICE: usage.total_price,
|
||||
NodeRunMetadataKey.CURRENCY: usage.currency,
|
||||
},
|
||||
llm_usage=usage,
|
||||
)
|
||||
)
|
||||
|
||||
def _invoke_llm(
|
||||
self,
|
||||
node_data_model: ModelConfig,
|
||||
|
||||
@ -1 +1,2 @@
|
||||
Single-database configuration for Flask.
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""add retry_index field to node-execution model
|
||||
|
||||
Revision ID: e1944c35e15e
|
||||
Revises: 11b07f66c737
|
||||
Create Date: 2024-12-20 06:28:30.287197
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
"""extend_provider_name_column
|
||||
|
||||
Revision ID: 4413929e1ec2
|
||||
Revises: 08ec4f75af5e
|
||||
Create Date: 2025-03-03 03:04:58.181493
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import models as models
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4413929e1ec2'
|
||||
down_revision = '08ec4f75af5e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op:
|
||||
batch_op.alter_column('provider_name',
|
||||
existing_type=sa.VARCHAR(length=40),
|
||||
type_=sa.String(length=255),
|
||||
existing_nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('dataset_collection_bindings', schema=None) as batch_op:
|
||||
batch_op.alter_column('provider_name',
|
||||
existing_type=sa.String(length=255),
|
||||
type_=sa.VARCHAR(length=40),
|
||||
existing_nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@ -785,7 +785,7 @@ class DatasetCollectionBinding(db.Model): # type: ignore[name-defined]
|
||||
)
|
||||
|
||||
id = db.Column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()"))
|
||||
provider_name = db.Column(db.String(40), nullable=False)
|
||||
provider_name = db.Column(db.String(255), nullable=False)
|
||||
model_name = db.Column(db.String(255), nullable=False)
|
||||
type = db.Column(db.String(40), server_default=db.text("'dataset'::character varying"), nullable=False)
|
||||
collection_name = db.Column(db.String(64), nullable=False)
|
||||
|
||||
@ -180,7 +180,7 @@ class Workflow(Base):
|
||||
features["file_upload"]["enabled"] = image_enabled
|
||||
features["file_upload"]["number_limits"] = image_number_limits
|
||||
features["file_upload"]["allowed_file_upload_methods"] = image_transfer_methods
|
||||
features["file_upload"]["allowed_file_types"] = ["image"]
|
||||
features["file_upload"]["allowed_file_types"] = features["file_upload"].get("allowed_file_types", ["image"])
|
||||
features["file_upload"]["allowed_file_extensions"] = []
|
||||
del features["file_upload"]["image"]
|
||||
self._features = json.dumps(features)
|
||||
@ -256,11 +256,10 @@ class Workflow(Base):
|
||||
|
||||
# decrypt secret variables value
|
||||
def decrypt_func(var):
|
||||
return (
|
||||
var.model_copy(update={"value": encrypter.decrypt_token(tenant_id=tenant_id, token=var.value)})
|
||||
if isinstance(var, SecretVariable)
|
||||
else var
|
||||
)
|
||||
if isinstance(var, SecretVariable):
|
||||
return var.model_copy(update={"value": encrypter.decrypt_token(tenant_id=tenant_id, token=var.value)})
|
||||
else:
|
||||
return var
|
||||
|
||||
results = list(map(decrypt_func, results))
|
||||
return results
|
||||
@ -286,11 +285,10 @@ class Workflow(Base):
|
||||
|
||||
# encrypt secret variables value
|
||||
def encrypt_func(var):
|
||||
return (
|
||||
var.model_copy(update={"value": encrypter.encrypt_token(tenant_id=tenant_id, token=var.value)})
|
||||
if isinstance(var, SecretVariable)
|
||||
else var
|
||||
)
|
||||
if isinstance(var, SecretVariable):
|
||||
return var.model_copy(update={"value": encrypter.encrypt_token(tenant_id=tenant_id, token=var.value)})
|
||||
else:
|
||||
return var
|
||||
|
||||
encrypted_vars = list(map(encrypt_func, value))
|
||||
environment_variables_json = json.dumps(
|
||||
|
||||
548
api/poetry.lock
generated
548
api/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -50,7 +50,7 @@ oci = "~2.135.1"
|
||||
openai = "~1.61.0"
|
||||
openpyxl = "~3.1.5"
|
||||
opik = "~1.3.4"
|
||||
pandas = { version = "~2.2.2", extras = ["performance", "excel"] }
|
||||
pandas = { version = "~2.2.2", extras = ["performance", "excel", "output-formatting"] }
|
||||
pandas-stubs = "~2.2.3.241009"
|
||||
psycogreen = "~1.0.2"
|
||||
psycopg2-binary = "~2.9.6"
|
||||
@ -174,4 +174,4 @@ types-tqdm = "~4.67.0.20241221"
|
||||
optional = true
|
||||
[tool.poetry.group.lint.dependencies]
|
||||
dotenv-linter = "~0.5.0"
|
||||
ruff = "~0.9.2"
|
||||
ruff = "~0.9.9"
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Mapping, Sequence
|
||||
from mimetypes import guess_type
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from configs import dify_config
|
||||
from core.helper import marketplace
|
||||
@ -18,11 +21,71 @@ from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginUploadRe
|
||||
from core.plugin.manager.asset import PluginAssetManager
|
||||
from core.plugin.manager.debugging import PluginDebuggingManager
|
||||
from core.plugin.manager.plugin import PluginInstallationManager
|
||||
from extensions.ext_redis import redis_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PluginService:
|
||||
class LatestPluginCache(BaseModel):
|
||||
plugin_id: str
|
||||
version: str
|
||||
unique_identifier: str
|
||||
|
||||
REDIS_KEY_PREFIX = "plugin_service:latest_plugin:"
|
||||
REDIS_TTL = 60 * 5 # 5 minutes
|
||||
|
||||
@staticmethod
|
||||
def fetch_latest_plugin_version(plugin_ids: Sequence[str]) -> Mapping[str, Optional[LatestPluginCache]]:
|
||||
"""
|
||||
Fetch the latest plugin version
|
||||
"""
|
||||
result: dict[str, Optional[PluginService.LatestPluginCache]] = {}
|
||||
|
||||
try:
|
||||
cache_not_exists = []
|
||||
|
||||
# Try to get from Redis first
|
||||
for plugin_id in plugin_ids:
|
||||
cached_data = redis_client.get(f"{PluginService.REDIS_KEY_PREFIX}{plugin_id}")
|
||||
if cached_data:
|
||||
result[plugin_id] = PluginService.LatestPluginCache.model_validate_json(cached_data)
|
||||
else:
|
||||
cache_not_exists.append(plugin_id)
|
||||
|
||||
if cache_not_exists:
|
||||
manifests = {
|
||||
manifest.plugin_id: manifest
|
||||
for manifest in marketplace.batch_fetch_plugin_manifests(cache_not_exists)
|
||||
}
|
||||
|
||||
for plugin_id, manifest in manifests.items():
|
||||
latest_plugin = PluginService.LatestPluginCache(
|
||||
plugin_id=plugin_id,
|
||||
version=manifest.latest_version,
|
||||
unique_identifier=manifest.latest_package_identifier,
|
||||
)
|
||||
|
||||
# Store in Redis
|
||||
redis_client.setex(
|
||||
f"{PluginService.REDIS_KEY_PREFIX}{plugin_id}",
|
||||
PluginService.REDIS_TTL,
|
||||
latest_plugin.model_dump_json(),
|
||||
)
|
||||
|
||||
result[plugin_id] = latest_plugin
|
||||
|
||||
# pop plugin_id from cache_not_exists
|
||||
cache_not_exists.remove(plugin_id)
|
||||
|
||||
for plugin_id in cache_not_exists:
|
||||
result[plugin_id] = None
|
||||
|
||||
return result
|
||||
except Exception:
|
||||
logger.exception("failed to fetch latest plugin version")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_debugging_key(tenant_id: str) -> str:
|
||||
"""
|
||||
@ -40,9 +103,7 @@ class PluginService:
|
||||
plugins = manager.list_plugins(tenant_id)
|
||||
plugin_ids = [plugin.plugin_id for plugin in plugins if plugin.source == PluginInstallationSource.Marketplace]
|
||||
try:
|
||||
manifests = {
|
||||
manifest.plugin_id: manifest for manifest in marketplace.batch_fetch_plugin_manifests(plugin_ids)
|
||||
}
|
||||
manifests = PluginService.fetch_latest_plugin_version(plugin_ids)
|
||||
except Exception:
|
||||
manifests = {}
|
||||
logger.exception("failed to fetch plugin manifests")
|
||||
@ -50,9 +111,11 @@ class PluginService:
|
||||
for plugin in plugins:
|
||||
if plugin.source == PluginInstallationSource.Marketplace:
|
||||
if plugin.plugin_id in manifests:
|
||||
# set latest_version
|
||||
plugin.latest_version = manifests[plugin.plugin_id].latest_version
|
||||
plugin.latest_unique_identifier = manifests[plugin.plugin_id].latest_package_identifier
|
||||
latest_plugin_cache = manifests[plugin.plugin_id]
|
||||
if latest_plugin_cache:
|
||||
# set latest_version
|
||||
plugin.latest_version = latest_plugin_cache.version
|
||||
plugin.latest_unique_identifier = latest_plugin_cache.unique_identifier
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
@ -87,7 +87,6 @@ ZHINAO_API_KEY=
|
||||
# Plugin configuration
|
||||
PLUGIN_DAEMON_KEY=
|
||||
PLUGIN_DAEMON_URL=
|
||||
INNER_API_KEY=
|
||||
|
||||
# Marketplace configuration
|
||||
MARKETPLACE_API_URL=
|
||||
|
||||
@ -13,11 +13,9 @@ class OracleVectorTest(AbstractVectorTest):
|
||||
self.vector = OracleVector(
|
||||
collection_name=self.collection_name,
|
||||
config=OracleVectorConfig(
|
||||
host="localhost",
|
||||
port=1521,
|
||||
user="dify",
|
||||
password="dify",
|
||||
database="FREEPDB1",
|
||||
dsn="localhost:1521/FREEPDB1",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -483,11 +483,13 @@ CHROMA_AUTH_PROVIDER=chromadb.auth.token_authn.TokenAuthClientProvider
|
||||
CHROMA_AUTH_CREDENTIALS=
|
||||
|
||||
# Oracle configuration, only available when VECTOR_STORE is `oracle`
|
||||
ORACLE_HOST=oracle
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_USER=dify
|
||||
ORACLE_PASSWORD=dify
|
||||
ORACLE_DATABASE=FREEPDB1
|
||||
ORACLE_DSN=oracle:1521/FREEPDB1
|
||||
ORACLE_CONFIG_DIR=/app/api/storage/wallet
|
||||
ORACLE_WALLET_LOCATION=/app/api/storage/wallet
|
||||
ORACLE_WALLET_PASSWORD=dify
|
||||
ORACLE_IS_AUTONOMOUS=false
|
||||
|
||||
# relyt configurations, only available when VECTOR_STORE is `relyt`
|
||||
RELYT_HOST=db
|
||||
|
||||
@ -12,6 +12,8 @@ services:
|
||||
SENTRY_DSN: ${API_SENTRY_DSN:-}
|
||||
SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0}
|
||||
SENTRY_PROFILES_SAMPLE_RATE: ${API_SENTRY_PROFILES_SAMPLE_RATE:-1.0}
|
||||
PLUGIN_REMOTE_INSTALL_HOST: ${EXPOSE_PLUGIN_DEBUGGING_HOST:-localhost}
|
||||
PLUGIN_REMOTE_INSTALL_PORT: ${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}
|
||||
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
|
||||
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
|
||||
depends_on:
|
||||
@ -142,8 +144,8 @@ services:
|
||||
PPROF_ENABLED: ${PLUGIN_PPROF_ENABLED:-false}
|
||||
DIFY_INNER_API_URL: ${PLUGIN_DIFY_INNER_API_URL:-http://api:5001}
|
||||
DIFY_INNER_API_KEY: ${INNER_API_KEY_FOR_PLUGIN:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
|
||||
PLUGIN_REMOTE_INSTALLING_HOST: ${PLUGIN_REMOTE_INSTALL_HOST:-0.0.0.0}
|
||||
PLUGIN_REMOTE_INSTALLING_PORT: ${PLUGIN_REMOTE_INSTALL_PORT:-5003}
|
||||
PLUGIN_REMOTE_INSTALLING_HOST: ${PLUGIN_DEBUGGING_HOST:-0.0.0.0}
|
||||
PLUGIN_REMOTE_INSTALLING_PORT: ${PLUGIN_DEBUGGING_PORT:-5003}
|
||||
PLUGIN_WORKING_PATH: ${PLUGIN_WORKING_PATH:-/app/storage/cwd}
|
||||
FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true}
|
||||
ports:
|
||||
|
||||
@ -197,11 +197,13 @@ x-shared-env: &shared-api-worker-env
|
||||
CHROMA_DATABASE: ${CHROMA_DATABASE:-default_database}
|
||||
CHROMA_AUTH_PROVIDER: ${CHROMA_AUTH_PROVIDER:-chromadb.auth.token_authn.TokenAuthClientProvider}
|
||||
CHROMA_AUTH_CREDENTIALS: ${CHROMA_AUTH_CREDENTIALS:-}
|
||||
ORACLE_HOST: ${ORACLE_HOST:-oracle}
|
||||
ORACLE_PORT: ${ORACLE_PORT:-1521}
|
||||
ORACLE_USER: ${ORACLE_USER:-dify}
|
||||
ORACLE_PASSWORD: ${ORACLE_PASSWORD:-dify}
|
||||
ORACLE_DATABASE: ${ORACLE_DATABASE:-FREEPDB1}
|
||||
ORACLE_DSN: ${ORACLE_DSN:-oracle:1521/FREEPDB1}
|
||||
ORACLE_CONFIG_DIR: ${ORACLE_CONFIG_DIR:-/app/api/storage/wallet}
|
||||
ORACLE_WALLET_LOCATION: ${ORACLE_WALLET_LOCATION:-/app/api/storage/wallet}
|
||||
ORACLE_WALLET_PASSWORD: ${ORACLE_WALLET_PASSWORD:-dify}
|
||||
ORACLE_IS_AUTONOMOUS: ${ORACLE_IS_AUTONOMOUS:-false}
|
||||
RELYT_HOST: ${RELYT_HOST:-db}
|
||||
RELYT_PORT: ${RELYT_PORT:-5432}
|
||||
RELYT_USER: ${RELYT_USER:-postgres}
|
||||
@ -424,6 +426,8 @@ services:
|
||||
SENTRY_DSN: ${API_SENTRY_DSN:-}
|
||||
SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0}
|
||||
SENTRY_PROFILES_SAMPLE_RATE: ${API_SENTRY_PROFILES_SAMPLE_RATE:-1.0}
|
||||
PLUGIN_REMOTE_INSTALL_HOST: ${EXPOSE_PLUGIN_DEBUGGING_HOST:-localhost}
|
||||
PLUGIN_REMOTE_INSTALL_PORT: ${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}
|
||||
PLUGIN_MAX_PACKAGE_SIZE: ${PLUGIN_MAX_PACKAGE_SIZE:-52428800}
|
||||
INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_DIFY_INNER_API_KEY:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
|
||||
depends_on:
|
||||
@ -554,8 +558,8 @@ services:
|
||||
PPROF_ENABLED: ${PLUGIN_PPROF_ENABLED:-false}
|
||||
DIFY_INNER_API_URL: ${PLUGIN_DIFY_INNER_API_URL:-http://api:5001}
|
||||
DIFY_INNER_API_KEY: ${INNER_API_KEY_FOR_PLUGIN:-QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1}
|
||||
PLUGIN_REMOTE_INSTALLING_HOST: ${PLUGIN_REMOTE_INSTALL_HOST:-0.0.0.0}
|
||||
PLUGIN_REMOTE_INSTALLING_PORT: ${PLUGIN_REMOTE_INSTALL_PORT:-5003}
|
||||
PLUGIN_REMOTE_INSTALLING_HOST: ${PLUGIN_DEBUGGING_HOST:-0.0.0.0}
|
||||
PLUGIN_REMOTE_INSTALLING_PORT: ${PLUGIN_DEBUGGING_PORT:-5003}
|
||||
PLUGIN_WORKING_PATH: ${PLUGIN_WORKING_PATH:-/app/storage/cwd}
|
||||
FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true}
|
||||
ports:
|
||||
|
||||
@ -5,6 +5,6 @@ create user dify identified by dify DEFAULT TABLESPACE users quota unlimited on
|
||||
grant DB_DEVELOPER_ROLE to dify;
|
||||
|
||||
BEGIN
|
||||
CTX_DDL.CREATE_PREFERENCE('my_chinese_vgram_lexer','CHINESE_VGRAM_LEXER');
|
||||
CTX_DDL.CREATE_PREFERENCE('dify.multilingual_lexer','CHINESE_VGRAM_LEXER');
|
||||
END;
|
||||
/
|
||||
|
||||
@ -42,7 +42,7 @@ const CSVDownload: FC = () => {
|
||||
<td className='h-9 pl-3 pr-2 border-b border-divider-regular'>{t('appAnnotation.batchModal.answer')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='text-gray-700'>
|
||||
<tbody className='text-text-secondary'>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.question')} 1</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.answer')} 1</td>
|
||||
|
||||
@ -219,7 +219,7 @@ const AppPublisher = ({
|
||||
)}
|
||||
<SuggestedAction
|
||||
onClick={() => {
|
||||
handleOpenInExplore()
|
||||
publishedAt && handleOpenInExplore()
|
||||
}}
|
||||
disabled={!publishedAt}
|
||||
icon={<RiPlanetLine className='w-4 h-4' />}
|
||||
|
||||
@ -132,11 +132,11 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
toggleSelect(item)
|
||||
}}
|
||||
>
|
||||
<div className='mr-1 flex items-center'>
|
||||
<div className='mr-1 flex items-center overflow-hidden'>
|
||||
<div className={cn('mr-2', !item.embedding_available && 'opacity-30')}>
|
||||
<TypeIcon type="upload_file" size='md' />
|
||||
</div>
|
||||
<div className={cn('max-w-[200px] text-[13px] font-medium text-text-secondary overflow-hidden text-ellipsis whitespace-nowrap', !item.embedding_available && 'opacity-30 !max-w-[120px]')}>{item.name}</div>
|
||||
<div className={cn('max-w-[200px] text-[13px] font-medium text-text-secondary truncate', !item.embedding_available && 'opacity-30 !max-w-[120px]')}>{item.name}</div>
|
||||
{!item.embedding_available && (
|
||||
<span className='ml-1 shrink-0 px-1 border border-divider-deep rounded-md text-text-tertiary text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
|
||||
)}
|
||||
@ -144,13 +144,14 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
{
|
||||
item.indexing_technique && (
|
||||
<Badge
|
||||
className='shrink-0'
|
||||
text={formatIndexingTechniqueAndMethod(item.indexing_technique, item.retrieval_model_dict?.search_method)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
item.provider === 'external' && (
|
||||
<Badge text={t('dataset.externalTag')} />
|
||||
<Badge className='shrink-0' text={t('dataset.externalTag')} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -124,18 +124,9 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
|
||||
doSend(v.payload.message, v.payload.files)
|
||||
})
|
||||
|
||||
const varList = modelConfig.configs.prompt_variables.map((item: any) => {
|
||||
return {
|
||||
label: item.key,
|
||||
value: inputs[item.key],
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<TextGeneration
|
||||
className='flex flex-col h-full overflow-y-auto border-none'
|
||||
innerClassName='grow flex flex-col'
|
||||
contentClassName='grow'
|
||||
content={completion}
|
||||
isLoading={!completion && isResponding}
|
||||
isResponding={isResponding}
|
||||
@ -144,8 +135,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
appId={appId}
|
||||
varList={varList}
|
||||
inSidePanel
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -516,9 +516,6 @@ const Debug: FC<IDebug> = ({
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
supportAnnotation
|
||||
appId={appId}
|
||||
varList={varList}
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -416,10 +416,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
supportFeedback
|
||||
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
|
||||
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
|
||||
supportAnnotation
|
||||
isShowTextToSpeech
|
||||
appId={appDetail?.id}
|
||||
varList={varList}
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { format } from '@/service/base'
|
||||
|
||||
export type ITextGenerationProps = {
|
||||
value: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TextGeneration: FC<ITextGenerationProps> = ({
|
||||
value,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: format(value),
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TextGeneration)
|
||||
@ -1,39 +1,40 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiBookmark3Line,
|
||||
RiClipboardLine,
|
||||
RiFileList3Line,
|
||||
RiPlayList2Line,
|
||||
RiReplay15Line,
|
||||
RiSparklingFill,
|
||||
RiSparklingLine,
|
||||
RiThumbDownLine,
|
||||
RiThumbUpLine,
|
||||
} from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { HashtagIcon } from '@heroicons/react/24/solid'
|
||||
import ResultTab from './result-tab'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import AudioBtn from '@/app/components/base/audio-btn'
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
|
||||
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
|
||||
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
|
||||
import { fetchTextGenerationMessage } from '@/service/debug'
|
||||
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { useChatContext } from '@/app/components/base/chat/chat/context'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const MAX_DEPTH = 3
|
||||
|
||||
export interface IGenerationItemProps {
|
||||
export type IGenerationItemProps = {
|
||||
isWorkflow?: boolean
|
||||
workflowProcessData?: WorkflowProcess
|
||||
className?: string
|
||||
@ -56,31 +57,12 @@ export interface IGenerationItemProps {
|
||||
taskId?: string
|
||||
controlClearMoreLikeThis?: number
|
||||
supportFeedback?: boolean
|
||||
supportAnnotation?: boolean
|
||||
isShowTextToSpeech?: boolean
|
||||
appId?: string
|
||||
varList?: { label: string; value: string | number | object }[]
|
||||
innerClassName?: string
|
||||
contentClassName?: string
|
||||
footerClassName?: string
|
||||
hideProcessDetail?: boolean
|
||||
siteInfo: SiteInfo | null
|
||||
inSidePanel?: boolean
|
||||
}
|
||||
|
||||
export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
|
||||
className?: string
|
||||
isDisabled?: boolean
|
||||
onClick?: () => void
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<div
|
||||
className={cn(isDisabled ? 'border-gray-100 text-gray-300' : 'border-gray-200 text-gray-700 cursor-pointer hover:border-gray-300 hover:shadow-sm', 'flex items-center h-7 px-3 rounded-md border text-xs font-medium', className)}
|
||||
onClick={() => !isDisabled && onClick?.()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const copyIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.3335 2.33341C9.87598 2.33341 10.1472 2.33341 10.3698 2.39304C10.9737 2.55486 11.4454 3.02657 11.6072 3.63048C11.6668 3.85302 11.6668 4.12426 11.6668 4.66675V10.0334C11.6668 11.0135 11.6668 11.5036 11.4761 11.8779C11.3083 12.2072 11.0406 12.4749 10.7113 12.6427C10.337 12.8334 9.84692 12.8334 8.86683 12.8334H5.1335C4.1534 12.8334 3.66336 12.8334 3.28901 12.6427C2.95973 12.4749 2.69201 12.2072 2.52423 11.8779C2.3335 11.5036 2.3335 11.0135 2.3335 10.0334V4.66675C2.3335 4.12426 2.3335 3.85302 2.39313 3.63048C2.55494 3.02657 3.02665 2.55486 3.63056 2.39304C3.8531 2.33341 4.12435 2.33341 4.66683 2.33341M5.60016 3.50008H8.40016C8.72686 3.50008 8.89021 3.50008 9.01499 3.4365C9.12475 3.38058 9.21399 3.29134 9.26992 3.18158C9.3335 3.05679 9.3335 2.89345 9.3335 2.56675V2.10008C9.3335 1.77338 9.3335 1.61004 9.26992 1.48525C9.21399 1.37549 9.12475 1.28625 9.01499 1.23033C8.89021 1.16675 8.72686 1.16675 8.40016 1.16675H5.60016C5.27347 1.16675 5.11012 1.16675 4.98534 1.23033C4.87557 1.28625 4.78634 1.37549 4.73041 1.48525C4.66683 1.61004 4.66683 1.77338 4.66683 2.10008V2.56675C4.66683 2.89345 4.66683 3.05679 4.73041 3.18158C4.78634 3.29134 4.87557 3.38058 4.98534 3.4365C5.11012 3.50008 5.27347 3.50008 5.60016 3.50008Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
@ -109,22 +91,16 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
taskId,
|
||||
controlClearMoreLikeThis,
|
||||
supportFeedback,
|
||||
supportAnnotation,
|
||||
isShowTextToSpeech,
|
||||
appId,
|
||||
varList,
|
||||
innerClassName,
|
||||
contentClassName,
|
||||
hideProcessDetail,
|
||||
siteInfo,
|
||||
inSidePanel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const params = useParams()
|
||||
const isTop = depth === 1
|
||||
const ref = useRef(null)
|
||||
const [completionRes, setCompletionRes] = useState('')
|
||||
const [childMessageId, setChildMessageId] = useState<string | null>(null)
|
||||
const hasChild = !!childMessageId
|
||||
const [childFeedback, setChildFeedback] = useState<FeedbackType>({
|
||||
rating: null,
|
||||
})
|
||||
@ -140,8 +116,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
setChildFeedback(childFeedback)
|
||||
}
|
||||
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
const question = (varList && varList?.length > 0) ? varList?.map(({ label, value }) => `${label}:${value}`).join('&') : ''
|
||||
const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
|
||||
|
||||
const childProps = {
|
||||
@ -161,6 +135,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
controlClearMoreLikeThis,
|
||||
isWorkflow,
|
||||
siteInfo,
|
||||
taskId,
|
||||
}
|
||||
|
||||
const handleMoreLikeThis = async () => {
|
||||
@ -178,19 +153,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
stopQuerying()
|
||||
}
|
||||
|
||||
const mainStyle = (() => {
|
||||
const res: React.CSSProperties = !isTop
|
||||
? {
|
||||
background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff',
|
||||
}
|
||||
: {}
|
||||
|
||||
if (hasChild)
|
||||
res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)'
|
||||
|
||||
return res
|
||||
})()
|
||||
|
||||
useEffect(() => {
|
||||
if (controlClearMoreLikeThis) {
|
||||
setChildMessageId(null)
|
||||
@ -228,123 +190,125 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
setShowPromptLogModal(true)
|
||||
}
|
||||
|
||||
const ratingContent = (
|
||||
<>
|
||||
{!isWorkflow && !isError && messageId && !feedback?.rating && (
|
||||
<SimpleBtn className="!px-0">
|
||||
<>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: 'like',
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: 'dislike',
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</>
|
||||
</SimpleBtn>
|
||||
)}
|
||||
{!isWorkflow && !isError && messageId && feedback?.rating === 'like' && (
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
)}
|
||||
{!isWorkflow && !isError && messageId && feedback?.rating === 'dislike' && (
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<string>('DETAIL')
|
||||
const showResultTabs = !!workflowProcessData?.resultText || !!workflowProcessData?.files?.length
|
||||
const switchTab = async (tab: string) => {
|
||||
setCurrentTab(tab)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (workflowProcessData?.resultText || !!workflowProcessData?.files?.length)
|
||||
switchTab('RESULT')
|
||||
else
|
||||
switchTab('DETAIL')
|
||||
}, [workflowProcessData?.files?.length, workflowProcessData?.resultText])
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn(isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-chat-bubble-bg' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0', className)}
|
||||
style={isTop
|
||||
? {
|
||||
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
|
||||
}
|
||||
: {}}
|
||||
>
|
||||
{isLoading
|
||||
? (
|
||||
<div className='flex items-center h-10'><Loading type='area' /></div>
|
||||
)
|
||||
: (
|
||||
<div
|
||||
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4', innerClassName)}
|
||||
style={mainStyle}
|
||||
>
|
||||
{(isTop && taskId) && (
|
||||
<div className='mb-2 text-gray-500 border border-gray-200 box-border flex items-center rounded-md italic text-[11px] pl-1 pr-1.5 font-medium w-fit group-hover:opacity-100'>
|
||||
<HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' />
|
||||
{taskId}
|
||||
</div>)
|
||||
}
|
||||
<div className={`flex ${contentClassName}`}>
|
||||
<div className='grow w-0'>
|
||||
{siteInfo && workflowProcessData && (
|
||||
<WorkflowProcessItem
|
||||
data={workflowProcessData}
|
||||
expand={workflowProcessData.expand}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
hideInfo={hideProcessDetail}
|
||||
readonly={!siteInfo.show_workflow_steps}
|
||||
/>
|
||||
)}
|
||||
{workflowProcessData && !isError && (
|
||||
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} />
|
||||
)}
|
||||
{isError && (
|
||||
<div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
|
||||
)}
|
||||
{!workflowProcessData && !isError && (typeof content === 'string') && (
|
||||
<>
|
||||
<div className={cn('relative', !isTop && 'mt-3', className)}>
|
||||
{isLoading && (
|
||||
<div className={cn('flex items-center h-10', !inSidePanel && 'bg-chat-bubble-bg rounded-2xl border-t border-divider-subtle')}><Loading type='area' /></div>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<>
|
||||
{/* result content */}
|
||||
<div className={cn(
|
||||
'relative',
|
||||
!inSidePanel && 'bg-chat-bubble-bg rounded-2xl border-t border-divider-subtle',
|
||||
)}>
|
||||
{workflowProcessData && (
|
||||
<>
|
||||
<div className={cn(
|
||||
'p-3 pb-0',
|
||||
showResultTabs && 'border-b border-divider-subtle',
|
||||
)}>
|
||||
{taskId && (
|
||||
<div className={cn('mb-2 flex items-center system-2xs-medium-uppercase text-text-accent-secondary', isError && 'text-text-destructive')}>
|
||||
<RiPlayList2Line className='w-3 h-3 mr-1' />
|
||||
<span>{t('share.generation.execution')}</span>
|
||||
<span className='px-1'>·</span>
|
||||
<span>{taskId}</span>
|
||||
</div>
|
||||
)}
|
||||
{siteInfo && workflowProcessData && (
|
||||
<WorkflowProcessItem
|
||||
data={workflowProcessData}
|
||||
expand={workflowProcessData.expand}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
hideInfo={hideProcessDetail}
|
||||
readonly={!siteInfo.show_workflow_steps}
|
||||
/>
|
||||
)}
|
||||
{showResultTabs && (
|
||||
<div className='flex items-center px-1 space-x-6'>
|
||||
<div
|
||||
className={cn(
|
||||
'py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer',
|
||||
currentTab === 'RESULT' && 'text-text-primary border-util-colors-blue-brand-blue-brand-600',
|
||||
)}
|
||||
onClick={() => switchTab('RESULT')}
|
||||
>{t('runLog.result')}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer',
|
||||
currentTab === 'DETAIL' && 'text-text-primary border-util-colors-blue-brand-blue-brand-600',
|
||||
)}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
>{t('runLog.detail')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isError && (
|
||||
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!workflowProcessData && taskId && (
|
||||
<div className={cn('sticky left-0 top-0 flex items-center w-full p-4 pb-3 bg-components-actionbar-bg rounded-t-2xl system-2xs-medium-uppercase text-text-accent-secondary', isError && 'text-text-destructive')}>
|
||||
<RiPlayList2Line className='w-3 h-3 mr-1' />
|
||||
<span>{t('share.generation.execution')}</span>
|
||||
<span className='px-1'>·</span>
|
||||
<span>{`${taskId}${depth > 1 ? `-${depth - 1}` : ''}`}</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && (
|
||||
<div className='p-4 pt-0 text-text-quaternary body-lg-regular'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
|
||||
)}
|
||||
{!workflowProcessData && !isError && (typeof content === 'string') && (
|
||||
<div className={cn('p-4', taskId && 'pt-0')}>
|
||||
<Markdown content={content} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between mt-3'>
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
!isInWebApp && !isInstalledApp && !isResponding && (
|
||||
<SimpleBtn
|
||||
isDisabled={isError || !messageId}
|
||||
className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')}
|
||||
onClick={handleOpenLogModal}>
|
||||
<File02 className='w-3.5 h-3.5' />
|
||||
{!isMobile && <div>{t('common.operation.log')}</div>}
|
||||
</SimpleBtn>
|
||||
)
|
||||
}
|
||||
{((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
|
||||
<SimpleBtn
|
||||
isDisabled={isError || !messageId}
|
||||
className={cn(isMobile && '!px-1.5', 'space-x-1')}
|
||||
onClick={() => {
|
||||
{/* meta data */}
|
||||
<div className={cn(
|
||||
'relative mt-1 h-4 px-4 text-text-quaternary system-xs-regular',
|
||||
isMobile && ((childMessageId || isQuerying) && depth < 3) && 'pl-10',
|
||||
)}>
|
||||
{!isWorkflow && <span>{content?.length} {t('common.unit.char')}</span>}
|
||||
{/* action buttons */}
|
||||
<div className='absolute right-2 bottom-1 flex items-center'>
|
||||
{!isInWebApp && !isInstalledApp && !isResponding && (
|
||||
<div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
|
||||
<ActionButton disabled={isError || !messageId} onClick={handleOpenLogModal}>
|
||||
<RiFileList3Line className='w-4 h-4' />
|
||||
{/* <div>{t('common.operation.log')}</div> */}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
<div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
|
||||
{moreLikeThis && (
|
||||
<ActionButton state={depth === MAX_DEPTH ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={depth === MAX_DEPTH} onClick={handleMoreLikeThis}>
|
||||
<RiSparklingLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isShowTextToSpeech && (
|
||||
<NewAudioButton
|
||||
id={messageId!}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
/>
|
||||
)}
|
||||
{((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
|
||||
<ActionButton disabled={isError || !messageId} onClick={() => {
|
||||
const copyContent = isWorkflow ? workflowProcessData?.resultText : content
|
||||
if (typeof copyContent === 'string')
|
||||
copy(copyContent)
|
||||
@ -352,117 +316,68 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
copy(JSON.stringify(copyContent))
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
|
||||
}}>
|
||||
<RiClipboardLine className='w-3.5 h-3.5' />
|
||||
{!isMobile && <div>{t('common.operation.copy')}</div>}
|
||||
</SimpleBtn>
|
||||
)}
|
||||
|
||||
{isInWebApp && (
|
||||
<>
|
||||
{!isWorkflow && (
|
||||
<SimpleBtn
|
||||
isDisabled={isError || !messageId}
|
||||
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
|
||||
onClick={() => { onSave?.(messageId as string) }}
|
||||
>
|
||||
<Bookmark className='w-3.5 h-3.5' />
|
||||
{!isMobile && <div>{t('common.operation.save')}</div>}
|
||||
</SimpleBtn>
|
||||
<RiClipboardLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isInWebApp && isError && (
|
||||
<ActionButton onClick={onRetry}>
|
||||
<RiReplay15Line className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isInWebApp && !isWorkflow && (
|
||||
<ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
|
||||
<RiBookmark3Line className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
{(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && (
|
||||
<div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
|
||||
{!feedback?.rating && (
|
||||
<>
|
||||
<ActionButton onClick={() => onFeedback?.({ rating: 'like' })}>
|
||||
<RiThumbUpLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={() => onFeedback?.({ rating: 'dislike' })}>
|
||||
<RiThumbDownLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{(moreLikeThis && depth < MAX_DEPTH) && (
|
||||
<SimpleBtn
|
||||
isDisabled={isError || !messageId}
|
||||
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
|
||||
onClick={handleMoreLikeThis}
|
||||
>
|
||||
<Stars02 className='w-3.5 h-3.5' />
|
||||
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
|
||||
</SimpleBtn>
|
||||
{feedback?.rating === 'like' && (
|
||||
<ActionButton state={ActionButtonState.Active} onClick={() => onFeedback?.({ rating: null })}>
|
||||
<RiThumbUpLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isError && (
|
||||
<SimpleBtn
|
||||
onClick={onRetry}
|
||||
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
|
||||
>
|
||||
<RefreshCcw01 className='w-3.5 h-3.5' />
|
||||
{!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
|
||||
</SimpleBtn>
|
||||
{feedback?.rating === 'dislike' && (
|
||||
<ActionButton state={ActionButtonState.Destructive} onClick={() => onFeedback?.({ rating: null })}>
|
||||
<RiThumbDownLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{!isError && messageId && !isWorkflow && (
|
||||
<div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
|
||||
)}
|
||||
{ratingContent}
|
||||
</>
|
||||
)}
|
||||
|
||||
{supportAnnotation && (
|
||||
<>
|
||||
<div className='ml-2 mr-1 h-[14px] w-[1px] bg-gray-200'></div>
|
||||
<AnnotationCtrlBtn
|
||||
appId={appId!}
|
||||
messageId={messageId!}
|
||||
className='ml-1'
|
||||
query={question}
|
||||
answer={content}
|
||||
// not support cache. So can not be cached
|
||||
cached={false}
|
||||
onAdded={() => {
|
||||
|
||||
}}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
onRemoved={() => { }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EditReplyModal
|
||||
appId={appId!}
|
||||
messageId={messageId!}
|
||||
isShow={isShowReplyModal}
|
||||
onHide={() => setIsShowReplyModal(false)}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={() => { }}
|
||||
onEdited={() => { }}
|
||||
createdAt={0}
|
||||
onRemove={() => { }}
|
||||
onlyEditResponse
|
||||
/>
|
||||
|
||||
{supportFeedback && (
|
||||
<div className='ml-1'>
|
||||
{ratingContent}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isShowTextToSpeech && (
|
||||
<>
|
||||
<div className='ml-2 mr-2 h-[14px] w-[1px] bg-gray-200'></div>
|
||||
<AudioBtn
|
||||
id={messageId!}
|
||||
className={'mr-1'}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!workflowProcessData && (
|
||||
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/* more like this elements */}
|
||||
{!isTop && (
|
||||
<div className={cn(
|
||||
'absolute top-[-32px] w-4 h-[33px] flex justify-center',
|
||||
isMobile ? 'left-[17px]' : 'left-[50%] translate-x-[-50%]',
|
||||
)}>
|
||||
<div className='h-full w-0.5 bg-divider-regular'></div>
|
||||
<div className={cn(
|
||||
'absolute left-0 w-4 h-4 flex items-center justify-center bg-util-colors-blue-blue-500 rounded-2xl border-[0.5px] border-divider-subtle shadow-xs',
|
||||
isMobile ? 'top-[3.5px]' : 'top-2',
|
||||
)}>
|
||||
<RiSparklingFill className='w-3 h-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
{((childMessageId || isQuerying) && depth < 3) && (
|
||||
<div className='pl-4'>
|
||||
<GenerationItem {...childProps as any} />
|
||||
</div>
|
||||
<GenerationItem {...childProps as any} />
|
||||
)}
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(GenerationItem)
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
@ -14,79 +11,45 @@ const ResultTab = ({
|
||||
data,
|
||||
content,
|
||||
currentTab,
|
||||
onCurrentTabChange,
|
||||
}: {
|
||||
data?: WorkflowProcess
|
||||
content: any
|
||||
currentTab: string
|
||||
onCurrentTabChange: (tab: string) => void
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const switchTab = async (tab: string) => {
|
||||
onCurrentTabChange(tab)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (data?.resultText || !!data?.files?.length)
|
||||
switchTab('RESULT')
|
||||
else
|
||||
switchTab('DETAIL')
|
||||
}, [data?.files?.length, data?.resultText])
|
||||
|
||||
return (
|
||||
<div className='grow relative flex flex-col'>
|
||||
{(data?.resultText || !!data?.files?.length) && (
|
||||
<div className='shrink-0 flex items-center mb-2 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
|
||||
currentTab === 'RESULT' && '!border-[rgb(21,94,239)] text-gray-700',
|
||||
)}
|
||||
onClick={() => switchTab('RESULT')}
|
||||
>{t('runLog.result')}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
|
||||
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-gray-700',
|
||||
)}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
>{t('runLog.detail')}</div>
|
||||
<>
|
||||
{currentTab === 'RESULT' && (
|
||||
<div className='p-4 space-y-3'>
|
||||
{data?.resultText && <Markdown content={data?.resultText || ''} />}
|
||||
{!!data?.files?.length && (
|
||||
<div className='flex flex-col gap-2'>
|
||||
{data?.files.map((item: any) => (
|
||||
<div key={item.varName} className='flex flex-col gap-1 system-xs-regular'>
|
||||
<div className='py-1 text-text-tertiary '>{item.varName}</div>
|
||||
<FileList
|
||||
files={item.list}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
canPreview
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('grow bg-white')}>
|
||||
{currentTab === 'RESULT' && (
|
||||
<>
|
||||
{data?.resultText && <Markdown content={data?.resultText || ''} />}
|
||||
{!!data?.files?.length && (
|
||||
<div className='flex flex-col gap-2'>
|
||||
{data?.files.map((item: any) => (
|
||||
<div key={item.varName} className='flex flex-col gap-1 system-xs-regular'>
|
||||
<div className='py-1 text-text-tertiary '>{item.varName}</div>
|
||||
<FileList
|
||||
files={item.list}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
canPreview
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentTab === 'DETAIL' && content && (
|
||||
<div className='mt-1'>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>JSON OUTPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={content}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{currentTab === 'DETAIL' && content && (
|
||||
<div className='p-4'>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>JSON OUTPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={content}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
RiClipboardLine,
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import NoData from './no-data'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { SavedMessage } from '@/models/debug'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { SimpleBtn, copyIcon } from '@/app/components/app/text-generate/item'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import AudioBtn from '@/app/components/base/audio-btn'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
|
||||
export type ISavedItemsProps = {
|
||||
className?: string
|
||||
@ -19,12 +23,6 @@ export type ISavedItemsProps = {
|
||||
onStartCreateContent: () => void
|
||||
}
|
||||
|
||||
const removeIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.25 1.75H8.75M1.75 3.5H12.25M11.0833 3.5L10.6742 9.63625C10.6129 10.5569 10.5822 11.0172 10.3833 11.3663C10.2083 11.6735 9.94422 11.9206 9.62597 12.0748C9.26448 12.25 8.80314 12.25 7.88045 12.25H6.11955C5.19686 12.25 4.73552 12.25 4.37403 12.0748C4.05577 11.9206 3.79172 11.6735 3.61666 11.3663C3.41781 11.0172 3.38713 10.5569 3.32575 9.63625L2.91667 3.5M5.83333 6.125V9.04167M8.16667 6.125V9.04167" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const SavedItems: FC<ISavedItemsProps> = ({
|
||||
className,
|
||||
isShowTextToSpeech,
|
||||
@ -35,56 +33,37 @@ const SavedItems: FC<ISavedItemsProps> = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'space-y-3')}>
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{list.length === 0
|
||||
? (
|
||||
<div className='px-6'>
|
||||
<NoData onStartCreateContent={onStartCreateContent} />
|
||||
</div>
|
||||
<NoData onStartCreateContent={onStartCreateContent} />
|
||||
)
|
||||
: (<>
|
||||
{list.map(({ id, answer }) => (
|
||||
<div
|
||||
key={id}
|
||||
className='p-4 rounded-xl bg-gray-50'
|
||||
style={{
|
||||
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
|
||||
}}
|
||||
>
|
||||
<Markdown content={answer} />
|
||||
<div className='flex items-center justify-between mt-3'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<SimpleBtn
|
||||
className='space-x-1'
|
||||
onClick={() => {
|
||||
copy(answer)
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
|
||||
}}>
|
||||
{copyIcon}
|
||||
<div>{t('common.operation.copy')}</div>
|
||||
</SimpleBtn>
|
||||
|
||||
<SimpleBtn
|
||||
className='space-x-1'
|
||||
onClick={() => {
|
||||
onRemove(id)
|
||||
}}>
|
||||
{removeIcon}
|
||||
<div>{t('common.operation.remove')}</div>
|
||||
</SimpleBtn>
|
||||
|
||||
{isShowTextToSpeech && (
|
||||
<>
|
||||
<div className='ml-2 mr-2 h-[14px] w-[1px] bg-gray-200'></div>
|
||||
<AudioBtn
|
||||
value={answer}
|
||||
noCache={false}
|
||||
className={'mr-1'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div key={id} className='relative'>
|
||||
<div className={cn(
|
||||
'p-4 bg-background-section-burn rounded-2xl',
|
||||
)}>
|
||||
<Markdown content={answer} />
|
||||
</div>
|
||||
<div className='mt-1 h-4 px-4 text-text-quaternary system-xs-regular'>
|
||||
<span>{answer.length} {t('common.unit.char')}</span>
|
||||
</div>
|
||||
<div className='absolute right-2 bottom-1'>
|
||||
<div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
|
||||
{isShowTextToSpeech && <NewAudioButton value={answer}/>}
|
||||
<ActionButton onClick={() => {
|
||||
copy(answer)
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
|
||||
}}>
|
||||
<RiClipboardLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={() => {
|
||||
onRemove(id)
|
||||
}}>
|
||||
<RiDeleteBinLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{answer?.length} {t('common.unit.char')}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -2,47 +2,38 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusIcon } from '@heroicons/react/24/outline'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiBookmark3Line,
|
||||
} from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
export type INoDataProps = {
|
||||
onStartCreateContent: () => void
|
||||
}
|
||||
|
||||
const markIcon = (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.16699 6.5C4.16699 5.09987 4.16699 4.3998 4.43948 3.86502C4.67916 3.39462 5.06161 3.01217 5.53202 2.77248C6.0668 2.5 6.76686 2.5 8.16699 2.5H11.8337C13.2338 2.5 13.9339 2.5 14.4686 2.77248C14.939 3.01217 15.3215 3.39462 15.5612 3.86502C15.8337 4.3998 15.8337 5.09987 15.8337 6.5V17.5L10.0003 14.1667L4.16699 17.5V6.5Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const lightIcon = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline relative -top-3 -left-1.5"><path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path></svg>
|
||||
)
|
||||
|
||||
const NoData: FC<INoDataProps> = ({
|
||||
onStartCreateContent,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mt-[60px] px-5 py-4 rounded-2xl bg-gray-50 '>
|
||||
<div className='flex items-center justify-center w-11 h-11 border border-gray-100 rounded-lg'>
|
||||
{markIcon}
|
||||
<div className='p-6 rounded-xl bg-background-section-burn '>
|
||||
<div className='flex items-center justify-center w-10 h-10 border-[0.5px] border-components-card-border bg-components-card-bg-alt rounded-[10px] shadow-lg backdrop-blur-sm'>
|
||||
<RiBookmark3Line className='w-4 h-4 text-text-accent'/>
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<span className='text-gray-700 font-semibold'>{t('share.generation.savedNoData.title')}</span>
|
||||
{lightIcon}
|
||||
<div className='mt-3'>
|
||||
<span className='text-text-secondary system-xl-semibold'>{t('share.generation.savedNoData.title')}</span>
|
||||
</div>
|
||||
<div className='mt-2 text-gray-500 text-[13px] font-normal'>
|
||||
<div className='mt-1 text-text-tertiary system-sm-regular'>
|
||||
{t('share.generation.savedNoData.description')}
|
||||
</div>
|
||||
<Button
|
||||
className='mt-4'
|
||||
variant='primary'
|
||||
className='mt-3'
|
||||
onClick={onStartCreateContent}
|
||||
>
|
||||
<div className='flex items-center space-x-2 text-primary-600 text-[13px] font-medium'>
|
||||
<PlusIcon className='w-4 h-4' />
|
||||
<span>{t('share.generation.savedNoData.startCreateContent')}</span>
|
||||
</div>
|
||||
<RiAddLine className='mr-1 w-4 h-4' />
|
||||
<span>{t('share.generation.savedNoData.startCreateContent')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -5,6 +5,10 @@
|
||||
@apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover
|
||||
}
|
||||
|
||||
.action-btn-hover {
|
||||
@apply bg-state-base-hover
|
||||
}
|
||||
|
||||
.action-btn-disabled {
|
||||
@apply cursor-not-allowed
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ enum ActionButtonState {
|
||||
Active = 'active',
|
||||
Disabled = 'disabled',
|
||||
Default = '',
|
||||
Hover = 'hover',
|
||||
}
|
||||
|
||||
const actionButtonVariants = cva(
|
||||
@ -41,6 +42,8 @@ function getActionButtonState(state: ActionButtonState) {
|
||||
return 'action-btn-active'
|
||||
case ActionButtonState.Disabled:
|
||||
return 'action-btn-disabled'
|
||||
case ActionButtonState.Hover:
|
||||
return 'action-btn-hover'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
.audioPlayer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--color-components-chat-input-audio-bg-alt);
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
min-width: 240px;
|
||||
max-width: 420px;
|
||||
max-height: 40px;
|
||||
backdrop-filter: blur(5px);
|
||||
border: 1px solid var(--color-components-panel-border-subtle);
|
||||
box-shadow: 0 1px 2px var(--color-shadow-shadow-3);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.playButton {
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-components-button-primary-bg);
|
||||
color: var(--color-components-chat-input-audio-bg-alt);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.1s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.playButton:hover {
|
||||
background-color: var(--color-components-button-primary-bg-hover);
|
||||
}
|
||||
|
||||
.playButton:disabled {
|
||||
background-color: var(--color-components-button-primary-bg-disabled);
|
||||
}
|
||||
|
||||
.audioControls {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.progressBarContainer {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.waveform {
|
||||
position: relative;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
border-radius: 2px;
|
||||
flex: none;
|
||||
order: 55;
|
||||
flex-grow: 0;
|
||||
height: 100%;
|
||||
background-color: rgba(66, 133, 244, 0.3);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.timeDisplay {
|
||||
/* position: absolute; */
|
||||
color: var(--color-text-accent-secondary);
|
||||
font-size: 12px;
|
||||
order: 0;
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* .currentTime {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 5px);
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(255,255,255,.8);
|
||||
padding: 2px 4px;
|
||||
border-radius:10px;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
|
||||
} */
|
||||
|
||||
.duration {
|
||||
padding: 2px 4px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.source_unavailable {
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
color: #bdbdbf;
|
||||
}
|
||||
|
||||
.playButton svg path,
|
||||
.playButton svg rect {
|
||||
fill: currentColor;
|
||||
}
|
||||
@ -1,7 +1,13 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { t } from 'i18next'
|
||||
import styles from './AudioPlayer.module.css'
|
||||
import {
|
||||
RiPauseCircleFill,
|
||||
RiPlayLargeFill,
|
||||
} from '@remixicon/react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type AudioPlayerProps = {
|
||||
src: string
|
||||
@ -18,6 +24,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
||||
const [hasStartedPlaying, setHasStartedPlaying] = useState(false)
|
||||
const [hoverTime, setHoverTime] = useState(0)
|
||||
const [isAudioAvailable, setIsAudioAvailable] = useState(true)
|
||||
const { theme } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
@ -230,11 +237,11 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
||||
let color
|
||||
|
||||
if (index * barWidth <= playedWidth)
|
||||
color = '#296DFF'
|
||||
color = theme === Theme.light ? '#296DFF' : '#84ABFF'
|
||||
else if ((index * barWidth / width) * duration <= hoverTime)
|
||||
color = 'rgba(21,90,239,.40)'
|
||||
color = theme === Theme.light ? 'rgba(21,90,239,.40)' : 'rgba(200, 206, 218, 0.28)'
|
||||
else
|
||||
color = 'rgba(21,90,239,.20)'
|
||||
color = theme === Theme.light ? 'rgba(21,90,239,.20)' : 'rgba(200, 206, 218, 0.14)'
|
||||
|
||||
const barHeight = value * height
|
||||
const rectX = index * barWidth
|
||||
@ -253,7 +260,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
||||
ctx.fillRect(rectX, rectY, rectWidth, rectHeight)
|
||||
}
|
||||
})
|
||||
}, [currentTime, duration, hoverTime, waveformData])
|
||||
}, [currentTime, duration, hoverTime, theme, waveformData])
|
||||
|
||||
useEffect(() => {
|
||||
drawWaveform()
|
||||
@ -279,40 +286,32 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
||||
}, [duration])
|
||||
|
||||
return (
|
||||
<div className={styles.audioPlayer}>
|
||||
<div className='flex items-end gap-2 h-9 min-w-[240px] max-w-[420px] p-2 bg-components-chat-input-audio-bg-alt backdrop-blur-sm rounded-[10px] border border-components-panel-border-subtle shadow-xs'>
|
||||
<audio ref={audioRef} src={src} preload="auto"/>
|
||||
<button className={styles.playButton} onClick={togglePlay} disabled={!isAudioAvailable}>
|
||||
<button className='shrink-0 inline-flex items-center justify-center border-none text-text-accent hover:text-text-accent-secondary transition-all cursor-pointer disabled:text-components-button-primary-bg-disabled' onClick={togglePlay} disabled={!isAudioAvailable}>
|
||||
{isPlaying
|
||||
? (
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<rect x="7" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
|
||||
<rect x="15" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
|
||||
</svg>
|
||||
<RiPauseCircleFill className='w-5 h-5' />
|
||||
)
|
||||
: (
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M8 5v14l11-7z" fill="currentColor"/>
|
||||
</svg>
|
||||
<RiPlayLargeFill className='w-5 h-5' />
|
||||
)}
|
||||
</button>
|
||||
<div className={isAudioAvailable ? styles.audioControls : styles.audioControls_disabled} hidden={!isAudioAvailable}>
|
||||
<div className={styles.progressBarContainer}>
|
||||
<div className={cn(isAudioAvailable && 'grow')} hidden={!isAudioAvailable}>
|
||||
<div className='h-8 flex items-center justify-center'>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={styles.waveform}
|
||||
className='relative grow h-6 w-full flex items-center justify-center cursor-pointer'
|
||||
onClick={handleCanvasInteraction}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseDown={handleCanvasInteraction}
|
||||
/>
|
||||
{/* <div className={styles.currentTime} style={{ left: `${(currentTime / duration) * 81}%`, bottom: '29px' }}>
|
||||
{formatTime(currentTime)}
|
||||
</div> */}
|
||||
<div className={styles.timeDisplay}>
|
||||
<span className={styles.duration}>{formatTime(duration)}</span>
|
||||
<div className='inline-flex items-center justify-center min-w-[50px] text-text-accent-secondary system-xs-medium'>
|
||||
<span className='px-0.5 py-1 rounded-[10px]'>{formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.source_unavailable} hidden={isAudioAvailable}>{t('common.operation.audioSourceUnavailable')}</div>
|
||||
<div className='absolute top-0 left-0 w-full h-full flex items-center justify-center text-text-quaternary' hidden={isAudioAvailable}>{t('common.operation.audioSourceUnavailable')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Chat from '../chat'
|
||||
import type {
|
||||
ChatConfig,
|
||||
@ -9,14 +9,17 @@ import type {
|
||||
import { useChat } from '../chat/hooks'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
|
||||
import { useChatWithHistoryContext } from './context'
|
||||
import Header from './header'
|
||||
import ConfigPanel from './config-panel'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
|
||||
import {
|
||||
fetchSuggestedQuestions,
|
||||
getUrl,
|
||||
stopChatMessageResponding,
|
||||
} from '@/service/share'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AnswerIcon from '@/app/components/base/answer-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const ChatWrapper = () => {
|
||||
const {
|
||||
@ -26,6 +29,7 @@ const ChatWrapper = () => {
|
||||
currentConversationItem,
|
||||
inputsForms,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationCompleted,
|
||||
isMobile,
|
||||
isInstalledApp,
|
||||
@ -65,6 +69,38 @@ const ChatWrapper = () => {
|
||||
appPrevChatTree,
|
||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||
)
|
||||
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
|
||||
const inputDisabled = useMemo(() => {
|
||||
let hasEmptyInput = ''
|
||||
let fileIsUploading = false
|
||||
const requiredVars = inputsForms.filter(({ required }) => required)
|
||||
if (requiredVars.length) {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (fileIsUploading)
|
||||
return
|
||||
|
||||
if (!inputsFormValue?.[variable])
|
||||
hasEmptyInput = label as string
|
||||
|
||||
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
|
||||
const files = inputsFormValue[variable]
|
||||
if (Array.isArray(files))
|
||||
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
|
||||
else
|
||||
fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
|
||||
}
|
||||
})
|
||||
}
|
||||
if (hasEmptyInput)
|
||||
return true
|
||||
|
||||
if (fileIsUploading)
|
||||
return true
|
||||
return false
|
||||
}, [inputsFormValue, inputsForms])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentChatInstanceRef.current)
|
||||
@ -107,42 +143,48 @@ const ChatWrapper = () => {
|
||||
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||
}, [chatList, doSend])
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (inputsForms.length) {
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
isMobile={isMobile}
|
||||
title={currentConversationItem?.name || ''}
|
||||
/>
|
||||
{
|
||||
!currentConversationId && (
|
||||
<div className={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}>
|
||||
<div className='mb-6' />
|
||||
<ConfigPanel />
|
||||
<div
|
||||
className='my-6 h-[1px]'
|
||||
style={{ background: 'linear-gradient(90deg, rgba(242, 244, 247, 0.00) 0%, #F2F4F7 49.17%, rgba(242, 244, 247, 0.00) 100%)' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const messageList = useMemo(() => {
|
||||
if (currentConversationId)
|
||||
return chatList
|
||||
return chatList.filter(item => !item.isOpeningStatement)
|
||||
}, [chatList, currentConversationId])
|
||||
|
||||
const [collapsed, setCollapsed] = useState(!!currentConversationId)
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (!inputsForms.length)
|
||||
return null
|
||||
if (isMobile) {
|
||||
if (!currentConversationId)
|
||||
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
return null
|
||||
}
|
||||
else {
|
||||
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
}
|
||||
}, [inputsForms.length, isMobile, currentConversationId, collapsed])
|
||||
|
||||
const welcome = useMemo(() => {
|
||||
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
||||
if (currentConversationId)
|
||||
return null
|
||||
if (!welcomeMessage)
|
||||
return null
|
||||
if (!collapsed && inputsForms.length > 0)
|
||||
return null
|
||||
return (
|
||||
<Header
|
||||
isMobile={isMobile}
|
||||
title={currentConversationItem?.name || ''}
|
||||
/>
|
||||
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
|
||||
<AppIcon
|
||||
size='xl'
|
||||
iconType={appData?.site.icon_type}
|
||||
icon={appData?.site.icon}
|
||||
background={appData?.site.icon_background}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
/>
|
||||
<div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
|
||||
</div>
|
||||
)
|
||||
}, [
|
||||
currentConversationId,
|
||||
inputsForms,
|
||||
currentConversationItem,
|
||||
isMobile,
|
||||
])
|
||||
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
|
||||
|
||||
const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
|
||||
? <AnswerIcon
|
||||
@ -160,17 +202,22 @@ const ChatWrapper = () => {
|
||||
<Chat
|
||||
appData={appData}
|
||||
config={appConfig}
|
||||
chatList={chatList}
|
||||
chatList={messageList}
|
||||
isResponding={isResponding}
|
||||
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
|
||||
chatFooterClassName='pb-4'
|
||||
chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}
|
||||
chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile ? 'px-2' : 'px-4'}`}
|
||||
onSend={doSend}
|
||||
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
|
||||
inputsForm={inputsForms}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
chatNode={chatNode}
|
||||
chatNode={
|
||||
<>
|
||||
{chatNode}
|
||||
{welcome}
|
||||
</>
|
||||
}
|
||||
allToolIcons={appMeta?.tool_icons || {}}
|
||||
onFeedback={handleFeedback}
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
@ -178,6 +225,8 @@ const ChatWrapper = () => {
|
||||
hideProcessDetail
|
||||
themeBuilder={themeBuilder}
|
||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
||||
inputDisabled={inputDisabled}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
interface InputProps {
|
||||
form: any
|
||||
value: string
|
||||
onChange: (variable: string, value: string) => void
|
||||
}
|
||||
const FormInput: FC<InputProps> = ({
|
||||
form,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
required,
|
||||
max_length,
|
||||
variable,
|
||||
} = form
|
||||
|
||||
if (type === 'paragraph') {
|
||||
return (
|
||||
<Textarea
|
||||
value={value}
|
||||
className='resize-none'
|
||||
onChange={e => onChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
className='grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none'
|
||||
value={value || ''}
|
||||
maxLength={max_length}
|
||||
onChange={e => onChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(FormInput)
|
||||
@ -1,117 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useChatWithHistoryContext } from '../context'
|
||||
import Input from './form-input'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
|
||||
const Form = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appParams,
|
||||
inputsForms,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationInputsChange,
|
||||
isMobile,
|
||||
} = useChatWithHistoryContext()
|
||||
|
||||
const handleFormChange = useCallback((variable: string, value: any) => {
|
||||
handleNewConversationInputsChange({
|
||||
...newConversationInputsRef.current,
|
||||
[variable]: value,
|
||||
})
|
||||
}, [newConversationInputsRef, handleNewConversationInputsChange])
|
||||
|
||||
const renderField = (form: any) => {
|
||||
const {
|
||||
label,
|
||||
required,
|
||||
variable,
|
||||
options,
|
||||
} = form
|
||||
|
||||
if (form.type === 'text-input' || form.type === 'paragraph') {
|
||||
return (
|
||||
<Input
|
||||
form={form}
|
||||
value={newConversationInputs[variable]}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (form.type === 'number') {
|
||||
return (
|
||||
<input
|
||||
className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
|
||||
type="number"
|
||||
value={newConversationInputs[variable] || ''}
|
||||
onChange={e => handleFormChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (form.type === InputVarType.singleFile) {
|
||||
return (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={newConversationInputs[variable] ? [newConversationInputs[variable]] : []}
|
||||
onChange={files => handleFormChange(variable, files[0])}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: 1,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (form.type === InputVarType.multiFiles) {
|
||||
return (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={newConversationInputs[variable]}
|
||||
onChange={files => handleFormChange(variable, files)}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: form.max_length,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalSelect
|
||||
popupClassName='w-[200px]'
|
||||
value={newConversationInputs[variable]}
|
||||
items={options.map((option: string) => ({ value: option, name: option }))}
|
||||
onSelect={item => handleFormChange(variable, item.value as string)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (!inputsForms.length)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-4 py-2'>
|
||||
{
|
||||
inputsForms.map(form => (
|
||||
<div
|
||||
key={form.variable}
|
||||
className={`flex mb-3 last-of-type:mb-0 text-sm text-gray-900 ${isMobile && '!flex-wrap'}`}
|
||||
>
|
||||
<div className={`shrink-0 mr-2 py-2 w-[128px] ${isMobile && '!w-full'}`}>{form.label}</div>
|
||||
{renderField(form)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@ -1,172 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useChatWithHistoryContext } from '../context'
|
||||
import Form from './form'
|
||||
import Button from '@/app/components/base/button'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Edit02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Star06 } from '@/app/components/base/icons/src/vender/solid/shapes'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
|
||||
const ConfigPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appData,
|
||||
inputsForms,
|
||||
handleStartChat,
|
||||
showConfigPanelBeforeChat,
|
||||
isMobile,
|
||||
} = useChatWithHistoryContext()
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
const customConfig = appData?.custom_config
|
||||
const site = appData?.site
|
||||
|
||||
return (
|
||||
<div className='flex flex-col max-h-[80%] w-full max-w-[720px]'>
|
||||
<div
|
||||
className={`
|
||||
grow rounded-xl overflow-y-auto
|
||||
${showConfigPanelBeforeChat && 'border-[0.5px] border-gray-100 shadow-lg'}
|
||||
${!showConfigPanelBeforeChat && collapsed && 'border border-indigo-100'}
|
||||
${!showConfigPanelBeforeChat && !collapsed && 'border-[0.5px] border-gray-100 shadow-lg'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25
|
||||
${isMobile && '!px-4 !py-3'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
showConfigPanelBeforeChat && (
|
||||
<>
|
||||
<div className='flex items-center h-8 text-2xl font-semibold text-gray-800'>
|
||||
<AppIcon
|
||||
iconType={appData?.site.icon_type}
|
||||
icon={appData?.site.icon}
|
||||
background='transparent'
|
||||
imageUrl={appData?.site.icon_url}
|
||||
size='small'
|
||||
className="mr-2"
|
||||
/>
|
||||
{appData?.site.title}
|
||||
</div>
|
||||
{
|
||||
appData?.site.description && (
|
||||
<div className='mt-2 w-full text-sm text-gray-500'>
|
||||
{appData?.site.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!showConfigPanelBeforeChat && collapsed && (
|
||||
<>
|
||||
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
|
||||
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
|
||||
{t('share.chat.configStatusDes')}
|
||||
</div>
|
||||
<Button
|
||||
variant='secondary-accent'
|
||||
size='small'
|
||||
className='shrink-0'
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
<Edit02 className='mr-1 w-3 h-3' />
|
||||
{t('common.operation.edit')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!showConfigPanelBeforeChat && !collapsed && (
|
||||
<>
|
||||
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
|
||||
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
|
||||
{t('share.chat.privatePromptConfigTitle')}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!collapsed && !showConfigPanelBeforeChat && (
|
||||
<div className='p-6 rounded-b-xl'>
|
||||
<Form />
|
||||
<div className={`pl-[136px] flex items-center ${isMobile && '!pl-0'}`}>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='mr-2'
|
||||
onClick={() => {
|
||||
setCollapsed(true)
|
||||
handleStartChat()
|
||||
}}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setCollapsed(true)}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
showConfigPanelBeforeChat && (
|
||||
<div className='p-6 rounded-b-xl'>
|
||||
<Form />
|
||||
<Button
|
||||
className={`${inputsForms.length && !isMobile && 'ml-[136px]'}`}
|
||||
variant='primary'
|
||||
size='large'
|
||||
onClick={handleStartChat}
|
||||
>
|
||||
<MessageDotsCircle className='mr-2 w-4 h-4 text-white' />
|
||||
{t('share.chat.startChat')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
showConfigPanelBeforeChat && (site || customConfig) && (
|
||||
<div className='mt-4 flex flex-wrap justify-between items-center py-2 text-xs text-gray-400'>
|
||||
{site?.privacy_policy
|
||||
? <div className={`flex items-center ${isMobile && 'w-full justify-end'}`}>{t('share.chat.privacyPolicyLeft')}
|
||||
<a
|
||||
className='text-gray-500 px-1'
|
||||
href={site?.privacy_policy}
|
||||
target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
|
||||
{t('share.chat.privacyPolicyRight')}
|
||||
</div>
|
||||
: <div>
|
||||
</div>}
|
||||
{
|
||||
customConfig?.remove_webapp_brand
|
||||
? null
|
||||
: (
|
||||
<div className={`flex items-center justify-end ${isMobile && 'w-full'}`}>
|
||||
<div className='flex items-center pr-3 space-x-3'>
|
||||
<span className='uppercase'>{t('share.chat.poweredBy')}</span>
|
||||
{
|
||||
customConfig?.replace_webapp_logo
|
||||
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
|
||||
: <LogoSite className='!h-5' />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfigPanel
|
||||
@ -28,13 +28,12 @@ export type ChatWithHistoryContextValue = {
|
||||
appPrevChatTree: ChatItemInTree[]
|
||||
pinnedConversationList: AppConversationData['data']
|
||||
conversationList: AppConversationData['data']
|
||||
showConfigPanelBeforeChat: boolean
|
||||
newConversationInputs: Record<string, any>
|
||||
newConversationInputsRef: RefObject<Record<string, any>>
|
||||
handleNewConversationInputsChange: (v: Record<string, any>) => void
|
||||
inputsForms: any[]
|
||||
handleNewConversation: () => void
|
||||
handleStartChat: () => void
|
||||
handleStartChat: (callback?: any) => void
|
||||
handleChangeConversation: (conversationId: string) => void
|
||||
handlePinConversation: (conversationId: string) => void
|
||||
handleUnpinConversation: (conversationId: string) => void
|
||||
@ -49,6 +48,8 @@ export type ChatWithHistoryContextValue = {
|
||||
handleFeedback: (messageId: string, feedback: Feedback) => void
|
||||
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
|
||||
themeBuilder?: ThemeBuilder
|
||||
sidebarCollapseState?: boolean
|
||||
handleSidebarCollapse: (state: boolean) => void
|
||||
}
|
||||
|
||||
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
|
||||
@ -56,7 +57,6 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
|
||||
appPrevChatTree: [],
|
||||
pinnedConversationList: [],
|
||||
conversationList: [],
|
||||
showConfigPanelBeforeChat: false,
|
||||
newConversationInputs: {},
|
||||
newConversationInputsRef: { current: {} },
|
||||
handleNewConversationInputsChange: () => {},
|
||||
@ -75,5 +75,7 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
|
||||
isInstalledApp: false,
|
||||
handleFeedback: () => {},
|
||||
currentChatInstanceRef: { current: { handleStop: () => {} } },
|
||||
sidebarCollapseState: false,
|
||||
handleSidebarCollapse: () => {},
|
||||
})
|
||||
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)
|
||||
|
||||
@ -1,60 +1,148 @@
|
||||
import { useState } from 'react'
|
||||
import { useChatWithHistoryContext } from './context'
|
||||
import Sidebar from './sidebar'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Edit05,
|
||||
Menu01,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
RiMenuLine,
|
||||
} from '@remixicon/react'
|
||||
import { useChatWithHistoryContext } from './context'
|
||||
import Operation from './header/operation'
|
||||
import Sidebar from './sidebar'
|
||||
import MobileOperationDropdown from './header/mobile-operation-dropdown'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
|
||||
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
|
||||
import type { ConversationItem } from '@/models/share'
|
||||
|
||||
const HeaderInMobile = () => {
|
||||
const {
|
||||
appData,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
pinnedConversationList,
|
||||
handleNewConversation,
|
||||
handlePinConversation,
|
||||
handleUnpinConversation,
|
||||
handleDeleteConversation,
|
||||
handleRenameConversation,
|
||||
conversationRenaming,
|
||||
} = useChatWithHistoryContext()
|
||||
const { t } = useTranslation()
|
||||
const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
|
||||
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
|
||||
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
|
||||
const handleOperate = useCallback((type: string) => {
|
||||
if (type === 'pin')
|
||||
handlePinConversation(currentConversationId)
|
||||
|
||||
if (type === 'unpin')
|
||||
handleUnpinConversation(currentConversationId)
|
||||
|
||||
if (type === 'delete')
|
||||
setShowConfirm(currentConversationItem as any)
|
||||
|
||||
if (type === 'rename')
|
||||
setShowRename(currentConversationItem as any)
|
||||
}, [currentConversationId, currentConversationItem, handlePinConversation, handleUnpinConversation])
|
||||
const handleCancelConfirm = useCallback(() => {
|
||||
setShowConfirm(null)
|
||||
}, [])
|
||||
const handleDelete = useCallback(() => {
|
||||
if (showConfirm)
|
||||
handleDeleteConversation(showConfirm.id, { onSuccess: handleCancelConfirm })
|
||||
}, [showConfirm, handleDeleteConversation, handleCancelConfirm])
|
||||
const handleCancelRename = useCallback(() => {
|
||||
setShowRename(null)
|
||||
}, [])
|
||||
const handleRename = useCallback((newName: string) => {
|
||||
if (showRename)
|
||||
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
|
||||
}, [showRename, handleRenameConversation, handleCancelRename])
|
||||
const [showSidebar, setShowSidebar] = useState(false)
|
||||
const [showChatSettings, setShowChatSettings] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='shrink-0 flex items-center px-3 h-[44px] border-b-[0.5px] border-b-gray-200'>
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg'
|
||||
onClick={() => setShowSidebar(true)}
|
||||
>
|
||||
<Menu01 className='w-4 h-4 text-gray-700' />
|
||||
<div className='shrink-0 flex items-center px-2 py-3 gap-1 bg-mask-top2bottom-gray-50-to-transparent'>
|
||||
<ActionButton size='l' className='shrink-0' onClick={() => setShowSidebar(true)}>
|
||||
<RiMenuLine className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
<div className='grow flex justify-center items-center'>
|
||||
{!currentConversationId && (
|
||||
<>
|
||||
<AppIcon
|
||||
className='mr-2'
|
||||
size='tiny'
|
||||
icon={appData?.site.icon}
|
||||
iconType={appData?.site.icon_type}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
background={appData?.site.icon_background}
|
||||
/>
|
||||
<div className='text-text-secondary system-md-semibold truncate'>
|
||||
{appData?.site.title}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{currentConversationId && (
|
||||
<Operation
|
||||
title={currentConversationItem?.name || ''}
|
||||
isPinned={!!isPin}
|
||||
togglePin={() => handleOperate(isPin ? 'unpin' : 'pin')}
|
||||
isShowDelete
|
||||
isShowRenameConversation
|
||||
onRenameConversation={() => handleOperate('rename')}
|
||||
onDelete={() => handleOperate('delete')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='grow flex justify-center items-center px-3'>
|
||||
<AppIcon
|
||||
className='mr-2'
|
||||
size='tiny'
|
||||
icon={appData?.site.icon}
|
||||
iconType={appData?.site.icon_type}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
background={appData?.site.icon_background}
|
||||
/>
|
||||
<div className='py-1 text-base font-semibold text-gray-800 truncate'>
|
||||
{appData?.site.title}
|
||||
<MobileOperationDropdown
|
||||
handleResetChat={handleNewConversation}
|
||||
handleViewChatSettings={() => setShowChatSettings(true)}
|
||||
/>
|
||||
</div>
|
||||
{showSidebar && (
|
||||
<div className='fixed inset-0 z-50 flex p-1 bg-background-overlay'
|
||||
onClick={() => setShowSidebar(false)}
|
||||
>
|
||||
<div className='flex h-full w-[calc(100vw_-_40px)] bg-components-panel-bg backdrop-blur-sm rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg'
|
||||
onClick={handleNewConversation}
|
||||
)}
|
||||
{showChatSettings && (
|
||||
<div className='fixed inset-0 z-50 flex justify-end p-1 bg-background-overlay'
|
||||
onClick={() => setShowChatSettings(false)}
|
||||
>
|
||||
<Edit05 className='w-4 h-4 text-gray-700' />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showSidebar && (
|
||||
<div className='fixed inset-0 z-50'
|
||||
style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }}
|
||||
onClick={() => setShowSidebar(false)}
|
||||
>
|
||||
<div className='inline-block h-full bg-white' onClick={e => e.stopPropagation()}>
|
||||
<Sidebar />
|
||||
<div className='flex flex-col h-full w-[calc(100vw_-_40px)] bg-components-panel-bg backdrop-blur-sm rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
|
||||
<div className='flex items-center gap-3 px-4 py-3 rounded-t-2xl border-b border-divider-subtle'>
|
||||
<Message3Fill className='shrink-0 w-6 h-6' />
|
||||
<div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<InputsFormContent showTip />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{!!showConfirm && (
|
||||
<Confirm
|
||||
title={t('share.chat.deleteConversation.title')}
|
||||
content={t('share.chat.deleteConversation.content') || ''}
|
||||
isShow
|
||||
onCancel={handleCancelConfirm}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
)}
|
||||
{showRename && (
|
||||
<RenameModal
|
||||
isShow
|
||||
onClose={handleCancelRename}
|
||||
saveLoading={conversationRenaming}
|
||||
name={showRename?.name || ''}
|
||||
onSave={handleRename}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
|
||||
type HeaderProps = {
|
||||
title: string
|
||||
isMobile: boolean
|
||||
}
|
||||
const Header: FC<HeaderProps> = ({
|
||||
title,
|
||||
isMobile,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
sticky top-0 flex items-center px-8 h-16 bg-white/80 text-base font-medium
|
||||
text-gray-900 border-b-[0.5px] border-b-gray-100 backdrop-blur-md z-10
|
||||
${isMobile && '!h-12'}
|
||||
`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Header)
|
||||
151
web/app/components/base/chat/chat-with-history/header/index.tsx
Normal file
151
web/app/components/base/chat/chat-with-history/header/index.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import {
|
||||
RiEditBoxLine,
|
||||
RiLayoutRight2Line,
|
||||
RiResetLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useChatWithHistoryContext,
|
||||
} from '../context'
|
||||
import Operation from './operation'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
|
||||
import type { ConversationItem } from '@/models/share'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const Header = () => {
|
||||
const {
|
||||
appData,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
inputsForms,
|
||||
pinnedConversationList,
|
||||
handlePinConversation,
|
||||
handleUnpinConversation,
|
||||
conversationRenaming,
|
||||
handleRenameConversation,
|
||||
handleDeleteConversation,
|
||||
handleNewConversation,
|
||||
sidebarCollapseState,
|
||||
handleSidebarCollapse,
|
||||
} = useChatWithHistoryContext()
|
||||
const { t } = useTranslation()
|
||||
const isSidebarCollapsed = sidebarCollapseState
|
||||
|
||||
const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
|
||||
|
||||
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
|
||||
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
|
||||
const handleOperate = useCallback((type: string) => {
|
||||
if (type === 'pin')
|
||||
handlePinConversation(currentConversationId)
|
||||
|
||||
if (type === 'unpin')
|
||||
handleUnpinConversation(currentConversationId)
|
||||
|
||||
if (type === 'delete')
|
||||
setShowConfirm(currentConversationItem as any)
|
||||
|
||||
if (type === 'rename')
|
||||
setShowRename(currentConversationItem as any)
|
||||
}, [currentConversationId, currentConversationItem, handlePinConversation, handleUnpinConversation])
|
||||
const handleCancelConfirm = useCallback(() => {
|
||||
setShowConfirm(null)
|
||||
}, [])
|
||||
const handleDelete = useCallback(() => {
|
||||
if (showConfirm)
|
||||
handleDeleteConversation(showConfirm.id, { onSuccess: handleCancelConfirm })
|
||||
}, [showConfirm, handleDeleteConversation, handleCancelConfirm])
|
||||
const handleCancelRename = useCallback(() => {
|
||||
setShowRename(null)
|
||||
}, [])
|
||||
const handleRename = useCallback((newName: string) => {
|
||||
if (showRename)
|
||||
handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
|
||||
}, [showRename, handleRenameConversation, handleCancelRename])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='shrink-0 h-14 p-3 flex items-center justify-between'>
|
||||
<div className={cn('flex items-center gap-1 transition-all duration-200 ease-in-out', !isSidebarCollapsed && 'opacity-0 user-select-none')}>
|
||||
<ActionButton className={cn(!isSidebarCollapsed && 'cursor-default')} size='l' onClick={() => handleSidebarCollapse(false)}>
|
||||
<RiLayoutRight2Line className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
<div className='shrink-0 mr-1'>
|
||||
<AppIcon
|
||||
size='large'
|
||||
iconType={appData?.site.icon_type}
|
||||
icon={appData?.site.icon}
|
||||
background={appData?.site.icon_background}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
/>
|
||||
</div>
|
||||
{!currentConversationId && (
|
||||
<div className={cn('grow text-text-secondary system-md-semibold truncate')}>{appData?.site.title}</div>
|
||||
)}
|
||||
{currentConversationId && currentConversationItem && isSidebarCollapsed && (
|
||||
<>
|
||||
<div className='p-1 text-divider-deep'>/</div>
|
||||
<Operation
|
||||
title={currentConversationItem?.name || ''}
|
||||
isPinned={!!isPin}
|
||||
togglePin={() => handleOperate(isPin ? 'unpin' : 'pin')}
|
||||
isShowDelete
|
||||
isShowRenameConversation
|
||||
onRenameConversation={() => handleOperate('rename')}
|
||||
onDelete={() => handleOperate('delete')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className='px-1 flex items-center'>
|
||||
<div className='h-[14px] w-px bg-divider-regular'></div>
|
||||
</div>
|
||||
{isSidebarCollapsed && (
|
||||
<ActionButton size='l' onClick={handleNewConversation}>
|
||||
<RiEditBoxLine className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
{currentConversationId && (
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<ActionButton size='l' onClick={handleNewConversation}>
|
||||
<RiResetLeftLine className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentConversationId && inputsForms.length > 0 && (
|
||||
<ViewFormDropdown />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!!showConfirm && (
|
||||
<Confirm
|
||||
title={t('share.chat.deleteConversation.title')}
|
||||
content={t('share.chat.deleteConversation.content') || ''}
|
||||
isShow
|
||||
onCancel={handleCancelConfirm}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
)}
|
||||
{showRename && (
|
||||
<RenameModal
|
||||
isShow
|
||||
onClose={handleCancelRename}
|
||||
saveLoading={conversationRenaming}
|
||||
name={showRename?.name || ''}
|
||||
onSave={handleRename}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@ -0,0 +1,55 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiMoreFill,
|
||||
} from '@remixicon/react'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
|
||||
type Props = {
|
||||
handleResetChat: () => void
|
||||
handleViewChatSettings: () => void
|
||||
}
|
||||
|
||||
const MobileOperationDropdown = ({
|
||||
handleResetChat,
|
||||
handleViewChatSettings,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: -4,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
|
||||
<RiMoreFill className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-40">
|
||||
<div
|
||||
className={'min-w-[160px] p-1 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'}
|
||||
>
|
||||
<div className='flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' onClick={handleResetChat}>
|
||||
<span className='grow'>{t('share.chat.resetChat')}</span>
|
||||
</div>
|
||||
<div className='flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' onClick={handleViewChatSettings}>
|
||||
<span className='grow'>{t('share.chat.viewChatSettings')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileOperationDropdown
|
||||
@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
isPinned: boolean
|
||||
isShowRenameConversation?: boolean
|
||||
onRenameConversation?: () => void
|
||||
isShowDelete: boolean
|
||||
togglePin: () => void
|
||||
onDelete: () => void
|
||||
placement?: Placement
|
||||
}
|
||||
|
||||
const Operation: FC<Props> = ({
|
||||
title,
|
||||
isPinned,
|
||||
togglePin,
|
||||
isShowRenameConversation,
|
||||
onRenameConversation,
|
||||
isShowDelete,
|
||||
onDelete,
|
||||
placement = 'bottom-start',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement={placement}
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<div className={cn('flex items-center p-1.5 pl-2 rounded-lg text-text-secondary cursor-pointer hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
|
||||
<div className='system-md-semibold'>{title}</div>
|
||||
<RiArrowDownSLine className='w-4 h-4 ' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-50">
|
||||
<div
|
||||
className={'min-w-[120px] p-1 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'}
|
||||
>
|
||||
<div className={cn('flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={togglePin}>
|
||||
<span className='grow'>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
|
||||
</div>
|
||||
{isShowRenameConversation && (
|
||||
<div className={cn('flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={onRenameConversation}>
|
||||
<span className='grow'>{t('explore.sidebar.action.rename')}</span>
|
||||
</div>
|
||||
)}
|
||||
{isShowDelete && (
|
||||
<div className={cn('group flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive')} onClick={onDelete} >
|
||||
<span className='grow'>{t('explore.sidebar.action.delete')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(Operation)
|
||||
@ -110,6 +110,19 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
changeLanguage(appData.site.default_language)
|
||||
}, [appData])
|
||||
|
||||
const [sidebarCollapseState, setSidebarCollapseState] = useState<boolean>(false)
|
||||
const handleSidebarCollapse = useCallback((state: boolean) => {
|
||||
if (appId) {
|
||||
setSidebarCollapseState(state)
|
||||
localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
|
||||
}
|
||||
}, [appId, setSidebarCollapseState])
|
||||
useEffect(() => {
|
||||
if (appId) {
|
||||
const localState = localStorage.getItem('webappSidebarCollapse')
|
||||
setSidebarCollapseState(localState === 'collapsed')
|
||||
}
|
||||
}, [appId])
|
||||
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, string>>(CONVERSATION_ID_INFO, {
|
||||
defaultValue: {},
|
||||
})
|
||||
@ -122,7 +135,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
})
|
||||
}
|
||||
}, [appId, conversationIdInfo, setConversationIdInfo])
|
||||
const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] = useState(true)
|
||||
|
||||
const [newConversationId, setNewConversationId] = useState('')
|
||||
const chatShouldReloadKey = useMemo(() => {
|
||||
@ -287,23 +299,18 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
|
||||
return true
|
||||
}, [inputsForms, notify, t])
|
||||
const handleStartChat = useCallback(() => {
|
||||
const handleStartChat = useCallback((callback: any) => {
|
||||
if (checkInputsRequired()) {
|
||||
setShowConfigPanelBeforeChat(false)
|
||||
setShowNewConversationItemInList(true)
|
||||
callback?.()
|
||||
}
|
||||
}, [setShowConfigPanelBeforeChat, setShowNewConversationItemInList, checkInputsRequired])
|
||||
}, [setShowNewConversationItemInList, checkInputsRequired])
|
||||
const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } })
|
||||
const handleChangeConversation = useCallback((conversationId: string) => {
|
||||
currentChatInstanceRef.current.handleStop()
|
||||
setNewConversationId('')
|
||||
handleConversationIdInfoChange(conversationId)
|
||||
|
||||
if (conversationId === '' && !checkInputsRequired(true))
|
||||
setShowConfigPanelBeforeChat(true)
|
||||
else
|
||||
setShowConfigPanelBeforeChat(false)
|
||||
}, [handleConversationIdInfoChange, setShowConfigPanelBeforeChat, checkInputsRequired])
|
||||
}, [handleConversationIdInfoChange])
|
||||
const handleNewConversation = useCallback(() => {
|
||||
currentChatInstanceRef.current.handleStop()
|
||||
setNewConversationId('')
|
||||
@ -313,11 +320,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}
|
||||
else if (currentConversationId) {
|
||||
handleConversationIdInfoChange('')
|
||||
setShowConfigPanelBeforeChat(true)
|
||||
setShowNewConversationItemInList(true)
|
||||
handleNewConversationInputsChange({})
|
||||
}
|
||||
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
|
||||
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
|
||||
const handleUpdateConversationList = useCallback(() => {
|
||||
mutateAppConversationData()
|
||||
mutateAppPinnedConversationData()
|
||||
@ -435,8 +441,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
appPrevChatTree,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
showConfigPanelBeforeChat,
|
||||
setShowConfigPanelBeforeChat,
|
||||
setShowNewConversationItemInList,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
@ -456,5 +460,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
chatShouldReloadKey,
|
||||
handleFeedback,
|
||||
currentChatInstanceRef,
|
||||
sidebarCollapseState,
|
||||
handleSidebarCollapse,
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,14 +11,15 @@ import {
|
||||
} from './context'
|
||||
import { useChatWithHistory } from './hooks'
|
||||
import Sidebar from './sidebar'
|
||||
import Header from './header'
|
||||
import HeaderInMobile from './header-in-mobile'
|
||||
import ConfigPanel from './config-panel'
|
||||
import ChatWrapper from './chat-wrapper'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { checkOrSetAccessToken } from '@/app/components/share/utils'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ChatWithHistoryProps = {
|
||||
className?: string
|
||||
@ -30,18 +31,18 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
||||
appInfoError,
|
||||
appData,
|
||||
appInfoLoading,
|
||||
appPrevChatTree,
|
||||
showConfigPanelBeforeChat,
|
||||
appChatListDataLoading,
|
||||
chatShouldReloadKey,
|
||||
isMobile,
|
||||
themeBuilder,
|
||||
sidebarCollapseState,
|
||||
} = useChatWithHistoryContext()
|
||||
|
||||
const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatTree.length)
|
||||
const isSidebarCollapsed = sidebarCollapseState
|
||||
const customConfig = appData?.custom_config
|
||||
const site = appData?.site
|
||||
|
||||
const [showSidePanel, setShowSidePanel] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted)
|
||||
if (site) {
|
||||
@ -65,35 +66,44 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`h-full flex bg-white ${className} ${isMobile && 'flex-col'}`}>
|
||||
{
|
||||
!isMobile && (
|
||||
<div className={cn(
|
||||
'h-full flex bg-background-default-burn',
|
||||
isMobile && 'flex-col',
|
||||
className,
|
||||
)}>
|
||||
{!isMobile && (
|
||||
<div className={cn(
|
||||
'flex flex-col w-[236px] p-1 pr-0 transition-all duration-200 ease-in-out',
|
||||
isSidebarCollapsed && 'w-0 !p-0 overflow-hidden',
|
||||
)}>
|
||||
<Sidebar />
|
||||
)
|
||||
}
|
||||
{
|
||||
isMobile && (
|
||||
<HeaderInMobile />
|
||||
)
|
||||
}
|
||||
<div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatTree.length && 'flex items-center justify-center'}`}>
|
||||
{
|
||||
showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatTree.length && (
|
||||
<div className={`flex w-full items-center justify-center h-full ${isMobile && 'px-4'}`}>
|
||||
<ConfigPanel />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
appChatListDataLoading && chatReady && (
|
||||
</div>
|
||||
)}
|
||||
{isMobile && (
|
||||
<HeaderInMobile />
|
||||
)}
|
||||
<div className={cn('relative grow p-2')}>
|
||||
{isSidebarCollapsed && (
|
||||
<div
|
||||
className={cn(
|
||||
'z-20 absolute top-0 w-[256px] h-full flex flex-col p-2 transition-all duration-500 ease-in-out',
|
||||
showSidePanel ? 'left-0' : 'left-[-248px]',
|
||||
)}
|
||||
onMouseEnter={() => setShowSidePanel(true)}
|
||||
onMouseLeave={() => setShowSidePanel(false)}
|
||||
>
|
||||
<Sidebar isPanel />
|
||||
</div>
|
||||
)}
|
||||
<div className='h-full flex flex-col bg-chatbot-bg rounded-2xl border-[0,5px] border-components-panel-border-subtle overflow-hidden'>
|
||||
{!isMobile && <Header />}
|
||||
{appChatListDataLoading && (
|
||||
<Loading type='app' />
|
||||
)
|
||||
}
|
||||
{
|
||||
chatReady && !appChatListDataLoading && (
|
||||
)}
|
||||
{!appChatListDataLoading && (
|
||||
<ChatWrapper key={chatShouldReloadKey} />
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -123,7 +133,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
||||
appPrevChatTree,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
showConfigPanelBeforeChat,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationInputsChange,
|
||||
@ -142,6 +151,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
||||
appId,
|
||||
handleFeedback,
|
||||
currentChatInstanceRef,
|
||||
sidebarCollapseState,
|
||||
handleSidebarCollapse,
|
||||
} = useChatWithHistory(installedAppInfo)
|
||||
|
||||
return (
|
||||
@ -157,7 +168,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
||||
appPrevChatTree,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
showConfigPanelBeforeChat,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationInputsChange,
|
||||
@ -178,6 +188,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
||||
handleFeedback,
|
||||
currentChatInstanceRef,
|
||||
themeBuilder,
|
||||
sidebarCollapseState,
|
||||
handleSidebarCollapse,
|
||||
}}>
|
||||
<ChatWithHistory className={className} />
|
||||
</ChatWithHistoryContext.Provider>
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useChatWithHistoryContext } from '../context'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
showTip?: boolean
|
||||
}
|
||||
|
||||
const InputsFormContent = ({ showTip }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appParams,
|
||||
inputsForms,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationInputsChange,
|
||||
} = useChatWithHistoryContext()
|
||||
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputs
|
||||
const readonly = !!currentConversationId
|
||||
|
||||
const handleFormChange = useCallback((variable: string, value: any) => {
|
||||
handleNewConversationInputsChange({
|
||||
...newConversationInputsRef.current,
|
||||
[variable]: value,
|
||||
})
|
||||
}, [newConversationInputsRef, handleNewConversationInputsChange])
|
||||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
{inputsForms.map(form => (
|
||||
<div key={form.variable} className='space-y-1'>
|
||||
<div className='h-6 flex items-center gap-1'>
|
||||
<div className='text-text-secondary system-md-semibold'>{form.label}</div>
|
||||
{!form.required && (
|
||||
<div className='text-text-tertiary system-xs-regular'>{t('appDebug.variableTable.optional')}</div>
|
||||
)}
|
||||
</div>
|
||||
{form.type === InputVarType.textInput && (
|
||||
<Input
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
placeholder={form.label}
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.number && (
|
||||
<Input
|
||||
type='number'
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
placeholder={form.label}
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.paragraph && (
|
||||
<Textarea
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
placeholder={form.label}
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.select && (
|
||||
<PortalSelect
|
||||
popupClassName='w-[200px]'
|
||||
value={inputsFormValue?.[form.variable]}
|
||||
items={form.options.map((option: string) => ({ value: option, name: option }))}
|
||||
onSelect={item => handleFormChange(form.variable, item.value as string)}
|
||||
placeholder={form.label}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.singleFile && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={inputsFormValue?.[form.variable] ? [inputsFormValue?.[form.variable]] : []}
|
||||
onChange={files => handleFormChange(form.variable, files[0])}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: 1,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{form.type === InputVarType.multiFiles && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={inputsFormValue?.[form.variable] || []}
|
||||
onChange={files => handleFormChange(form.variable, files)}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: form.max_length,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{showTip && (
|
||||
<div className='text-text-tertiary system-xs-regular'>{t('share.chat.chatFormTip')}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputsFormContent
|
||||
@ -0,0 +1,79 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
|
||||
import { useChatWithHistoryContext } from '../context'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
collapsed: boolean
|
||||
setCollapsed: (collapsed: boolean) => void
|
||||
}
|
||||
|
||||
const InputsFormNode = ({
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
isMobile,
|
||||
currentConversationId,
|
||||
handleStartChat,
|
||||
themeBuilder,
|
||||
} = useChatWithHistoryContext()
|
||||
|
||||
return (
|
||||
<div className={cn('pt-6 px-4 flex flex-col items-center', isMobile && 'pt-4')}>
|
||||
<div className={cn(
|
||||
'w-full max-w-[672px] bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-md',
|
||||
collapsed && 'bg-components-card-bg border border-components-card-border shadow-none',
|
||||
)}>
|
||||
<div className={cn(
|
||||
'flex items-center gap-3 px-6 py-4 rounded-t-2xl',
|
||||
!collapsed && 'border-b border-divider-subtle',
|
||||
isMobile && 'px-4 py-3',
|
||||
)}>
|
||||
<Message3Fill className='shrink-0 w-6 h-6' />
|
||||
<div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
|
||||
{collapsed && (
|
||||
<Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(false)}>{currentConversationId ? t('common.operation.view') : t('common.operation.edit')}</Button>
|
||||
)}
|
||||
{!collapsed && currentConversationId && (
|
||||
<Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(true)}>{t('common.operation.close')}</Button>
|
||||
)}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className={cn('p-6', isMobile && 'p-4')}>
|
||||
<InputsFormContent showTip={!!currentConversationId} />
|
||||
</div>
|
||||
)}
|
||||
{!collapsed && !currentConversationId && (
|
||||
<div className={cn('p-6', isMobile && 'p-4')}>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
onClick={() => handleStartChat(() => setCollapsed(true))}
|
||||
style={
|
||||
themeBuilder?.theme
|
||||
? {
|
||||
backgroundColor: themeBuilder?.theme.primaryColor,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>{t('share.chat.startChat')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{collapsed && (
|
||||
<div className='py-4 flex items-center w-full max-w-[720px]'>
|
||||
<Divider bgStyle='gradient' className='basis-1/2 h-px rotate-180' />
|
||||
<Divider bgStyle='gradient' className='basis-1/2 h-px' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputsFormNode
|
||||
@ -0,0 +1,48 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiChatSettingsLine,
|
||||
} from '@remixicon/react'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
|
||||
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
|
||||
|
||||
const ViewFormDropdown = () => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 4,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
|
||||
<RiChatSettingsLine className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-50">
|
||||
<div className='w-[400px] bg-components-panel-bg backdrop-blur-sm rounded-2xl border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
<div className='flex items-center gap-3 px-6 py-4 rounded-t-2xl border-b border-divider-subtle'>
|
||||
<Message3Fill className='shrink-0 w-6 h-6' />
|
||||
<div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
|
||||
</div>
|
||||
<div className='p-6'>
|
||||
<InputsFormContent showTip />
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewFormDropdown
|
||||
@ -3,22 +3,34 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiEditBoxLine,
|
||||
RiExpandRightLine,
|
||||
RiLayoutLeft2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useChatWithHistoryContext } from '../context'
|
||||
import List from './list'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Edit05 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { ConversationItem } from '@/models/share'
|
||||
import List from '@/app/components/base/chat/chat-with-history/sidebar/list'
|
||||
import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
import type { ConversationItem } from '@/models/share'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const Sidebar = () => {
|
||||
type Props = {
|
||||
isPanel?: boolean
|
||||
}
|
||||
|
||||
const Sidebar = ({ isPanel }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appData,
|
||||
handleNewConversation,
|
||||
pinnedConversationList,
|
||||
conversationList,
|
||||
handleNewConversation,
|
||||
currentConversationId,
|
||||
handleChangeConversation,
|
||||
handlePinConversation,
|
||||
@ -26,8 +38,12 @@ const Sidebar = () => {
|
||||
conversationRenaming,
|
||||
handleRenameConversation,
|
||||
handleDeleteConversation,
|
||||
sidebarCollapseState,
|
||||
handleSidebarCollapse,
|
||||
isMobile,
|
||||
} = useChatWithHistoryContext()
|
||||
const isSidebarCollapsed = sidebarCollapseState
|
||||
|
||||
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
|
||||
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
|
||||
|
||||
@ -60,66 +76,83 @@ const Sidebar = () => {
|
||||
}, [showRename, handleRenameConversation, handleCancelRename])
|
||||
|
||||
return (
|
||||
<div className='shrink-0 h-full flex flex-col w-[240px] border-r border-r-gray-100'>
|
||||
{
|
||||
!isMobile && (
|
||||
<div className='shrink-0 flex p-4'>
|
||||
<AppIcon
|
||||
className='mr-3'
|
||||
size='small'
|
||||
iconType={appData?.site.icon_type}
|
||||
icon={appData?.site.icon}
|
||||
background={appData?.site.icon_background}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
/>
|
||||
<div className='py-1 text-base font-semibold text-gray-800'>
|
||||
{appData?.site.title}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='shrink-0 p-4'>
|
||||
<Button
|
||||
variant='secondary-accent'
|
||||
className='justify-start w-full'
|
||||
onClick={handleNewConversation}
|
||||
>
|
||||
<Edit05 className='mr-2 w-4 h-4' />
|
||||
<div className={cn(
|
||||
'grow flex flex-col',
|
||||
isPanel && 'rounded-xl bg-components-panel-bg border-[0.5px] border-components-panel-border-subtle shadow-lg',
|
||||
)}>
|
||||
<div className={cn(
|
||||
'shrink-0 flex items-center gap-3 p-3 pr-2',
|
||||
)}>
|
||||
<div className='shrink-0'>
|
||||
<AppIcon
|
||||
size='large'
|
||||
iconType={appData?.site.icon_type}
|
||||
icon={appData?.site.icon}
|
||||
background={appData?.site.icon_background}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('grow text-text-secondary system-md-semibold truncate')}>{appData?.site.title}</div>
|
||||
{!isMobile && isSidebarCollapsed && (
|
||||
<ActionButton size='l' onClick={() => handleSidebarCollapse(false)}>
|
||||
<RiExpandRightLine className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{!isMobile && !isSidebarCollapsed && (
|
||||
<ActionButton size='l' onClick={() => handleSidebarCollapse(true)}>
|
||||
<RiLayoutLeft2Line className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className='shrink-0 px-3 py-4'>
|
||||
<Button variant='secondary-accent' className='w-full justify-center' onClick={handleNewConversation}>
|
||||
<RiEditBoxLine className='w-4 h-4 mr-1' />
|
||||
{t('share.chat.newChat')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='grow px-4 py-2 overflow-y-auto'>
|
||||
{
|
||||
!!pinnedConversationList.length && (
|
||||
<div className='mb-4'>
|
||||
<List
|
||||
isPin
|
||||
title={t('share.chat.pinnedTitle') || ''}
|
||||
list={pinnedConversationList}
|
||||
onChangeConversation={handleChangeConversation}
|
||||
onOperate={handleOperate}
|
||||
currentConversationId={currentConversationId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!conversationList.length && (
|
||||
<div className='grow h-0 pt-4 px-3 space-y-2 overflow-y-auto'>
|
||||
{/* pinned list */}
|
||||
{!!pinnedConversationList.length && (
|
||||
<div className='mb-4'>
|
||||
<List
|
||||
title={(pinnedConversationList.length && t('share.chat.unpinnedTitle')) || ''}
|
||||
list={conversationList}
|
||||
isPin
|
||||
title={t('share.chat.pinnedTitle') || ''}
|
||||
list={pinnedConversationList}
|
||||
onChangeConversation={handleChangeConversation}
|
||||
onOperate={handleOperate}
|
||||
currentConversationId={currentConversationId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{!!conversationList.length && (
|
||||
<List
|
||||
title={(pinnedConversationList.length && t('share.chat.unpinnedTitle')) || ''}
|
||||
list={conversationList}
|
||||
onChangeConversation={handleChangeConversation}
|
||||
onOperate={handleOperate}
|
||||
currentConversationId={currentConversationId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{appData?.site.copyright && (
|
||||
<div className='px-4 pb-4 text-xs text-gray-400'>
|
||||
© {(new Date()).getFullYear()} {appData?.site.copyright}
|
||||
<div className='shrink-0 p-3 flex items-center justify-between'>
|
||||
<MenuDropdown placement='top-start' data={appData?.site} />
|
||||
{/* powered by */}
|
||||
<div className='shrink-0'>
|
||||
{!appData?.custom_config?.remove_webapp_brand && (
|
||||
<div className={cn(
|
||||
'shrink-0 px-2 flex items-center gap-1.5',
|
||||
)}>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
|
||||
{appData?.custom_config?.replace_webapp_logo && (
|
||||
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
|
||||
)}
|
||||
{!appData?.custom_config?.replace_webapp_logo && (
|
||||
<LogoSite className='!h-5' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!!showConfirm && (
|
||||
<Confirm
|
||||
title={t('share.chat.deleteConversation.title')}
|
||||
|
||||
@ -5,8 +5,8 @@ import {
|
||||
} from 'react'
|
||||
import { useHover } from 'ahooks'
|
||||
import type { ConversationItem } from '@/models/share'
|
||||
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import ItemOperation from '@/app/components/explore/item-operation'
|
||||
import Operation from '@/app/components/base/chat/chat-with-history/sidebar/operation'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ItemProps = {
|
||||
isPin?: boolean
|
||||
@ -24,23 +24,23 @@ const Item: FC<ItemProps> = ({
|
||||
}) => {
|
||||
const ref = useRef(null)
|
||||
const isHovering = useHover(ref)
|
||||
const isSelected = currentConversationId === item.id
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
key={item.id}
|
||||
className={`
|
||||
flex mb-0.5 last-of-type:mb-0 py-1.5 pl-3 pr-1.5 text-sm font-medium text-gray-700
|
||||
rounded-lg cursor-pointer hover:bg-gray-50 group
|
||||
${currentConversationId === item.id && 'text-primary-600 bg-primary-50'}
|
||||
`}
|
||||
className={cn(
|
||||
'group flex p-1 pl-3 rounded-lg cursor-pointer text-components-menu-item-text system-sm-medium hover:bg-state-base-hover',
|
||||
isSelected && 'bg-state-accent-active hover:bg-state-accent-active text-text-accent',
|
||||
)}
|
||||
onClick={() => onChangeConversation(item.id)}
|
||||
>
|
||||
<MessageDotsCircle className={`shrink-0 mt-1 mr-2 w-4 h-4 text-gray-400 ${currentConversationId === item.id && 'text-primary-600'}`} />
|
||||
<div className='grow py-0.5 break-all' title={item.name}>{item.name}</div>
|
||||
<div className='grow p-1 pl-0 truncate' title={item.name}>{item.name}</div>
|
||||
{item.id !== '' && (
|
||||
<div className='shrink-0 h-6' onClick={e => e.stopPropagation()}>
|
||||
<ItemOperation
|
||||
<div className='shrink-0' onClick={e => e.stopPropagation()}>
|
||||
<Operation
|
||||
isActive={isSelected}
|
||||
isPinned={!!isPin}
|
||||
isItemHovering={isHovering}
|
||||
togglePin={() => onOperate(isPin ? 'unpin' : 'pin', item)}
|
||||
|
||||
@ -19,26 +19,20 @@ const List: FC<ListProps> = ({
|
||||
currentConversationId,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
title && (
|
||||
<div className='mb-0.5 px-3 h-[26px] text-xs font-medium text-gray-500'>
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
list.map(item => (
|
||||
<Item
|
||||
key={item.id}
|
||||
isPin={isPin}
|
||||
item={item}
|
||||
onOperate={onOperate}
|
||||
onChangeConversation={onChangeConversation}
|
||||
currentConversationId={currentConversationId}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<div className='space-y-0.5'>
|
||||
{title && (
|
||||
<div className='px-3 pt-2 pb-1 text-text-tertiary system-xs-medium-uppercase'>{title}</div>
|
||||
)}
|
||||
{list.map(item => (
|
||||
<Item
|
||||
key={item.id}
|
||||
isPin={isPin}
|
||||
item={item}
|
||||
onOperate={onOperate}
|
||||
onChangeConversation={onChangeConversation}
|
||||
currentConversationId={currentConversationId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,101 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
RiMoreFill,
|
||||
RiPushpinLine,
|
||||
RiUnpinLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
isActive?: boolean
|
||||
isItemHovering?: boolean
|
||||
isPinned: boolean
|
||||
isShowRenameConversation?: boolean
|
||||
onRenameConversation?: () => void
|
||||
isShowDelete: boolean
|
||||
togglePin: () => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
const Operation: FC<Props> = ({
|
||||
isActive,
|
||||
isItemHovering,
|
||||
isPinned,
|
||||
togglePin,
|
||||
isShowRenameConversation,
|
||||
onRenameConversation,
|
||||
isShowDelete,
|
||||
onDelete,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef(null)
|
||||
const [isHovering, { setTrue: setIsHovering, setFalse: setNotHovering }] = useBoolean(false)
|
||||
useEffect(() => {
|
||||
if (!isItemHovering && !isHovering)
|
||||
setOpen(false)
|
||||
}, [isItemHovering, isHovering])
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<ActionButton
|
||||
className={cn((isItemHovering || open) ? 'opacity-100' : 'opacity-0')}
|
||||
state={
|
||||
isActive
|
||||
? ActionButtonState.Active
|
||||
: open
|
||||
? ActionButtonState.Hover
|
||||
: ActionButtonState.Default
|
||||
}
|
||||
>
|
||||
<RiMoreFill className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-50">
|
||||
<div
|
||||
ref={ref}
|
||||
className={'min-w-[120px] p-1 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'}
|
||||
onMouseEnter={setIsHovering}
|
||||
onMouseLeave={setNotHovering}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div className={cn('flex items-center space-x-1 px-2 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={togglePin}>
|
||||
{isPinned && <RiUnpinLine className='shrink-0 w-4 h-4 text-text-tertiary' />}
|
||||
{!isPinned && <RiPushpinLine className='shrink-0 w-4 h-4 text-text-tertiary' />}
|
||||
<span className='grow'>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
|
||||
</div>
|
||||
{isShowRenameConversation && (
|
||||
<div className={cn('flex items-center space-x-1 px-2 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={onRenameConversation}>
|
||||
<RiEditLine className='shrink-0 w-4 h-4 text-text-tertiary' />
|
||||
<span className='grow'>{t('explore.sidebar.action.rename')}</span>
|
||||
</div>
|
||||
)}
|
||||
{isShowDelete && (
|
||||
<div className={cn('group flex items-center space-x-1 px-2 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive')} onClick={onDelete} >
|
||||
<RiDeleteBinLine className={cn('shrink-0 w-4 h-4 text-text-tertiary group-hover:text-text-destructive')} />
|
||||
<span className='grow'>{t('explore.sidebar.action.delete')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(Operation)
|
||||
@ -4,6 +4,7 @@ import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
||||
export type IRenameModalProps = {
|
||||
isShow: boolean
|
||||
@ -29,16 +30,16 @@ const RenameModal: FC<IRenameModalProps> = ({
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={'mt-6 font-medium text-sm leading-[21px] text-gray-900'}>{t('common.chat.conversationName')}</div>
|
||||
<input className={'mt-2 w-full rounded-lg h-10 box-border px-3 text-sm leading-10 bg-gray-100'}
|
||||
<div className={'mt-6 font-medium text-sm leading-[21px] text-text-primary'}>{t('common.chat.conversationName')}</div>
|
||||
<Input className='mt-2 w-full h-10'
|
||||
value={tempName}
|
||||
onChange={e => setTempName(e.target.value)}
|
||||
placeholder={t('common.chat.conversationNamePlaceholder') || ''}
|
||||
/>
|
||||
|
||||
<div className='mt-10 flex justify-end'>
|
||||
<Button className='mr-2 flex-shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' className='flex-shrink-0' onClick={() => onSave(tempName)} loading={saveLoading}>{t('common.operation.save')}</Button>
|
||||
<Button className='mr-2 shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' className='shrink-0' onClick={() => onSave(tempName)} loading={saveLoading}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@ -105,7 +105,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
<div className='shrink-0 relative w-10 h-10'>
|
||||
{answerIcon || <AnswerIcon />}
|
||||
{responding && (
|
||||
<div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-white rounded-full shadow-xs border-[0.5px] border-gray-50'>
|
||||
<div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-background-section-burn rounded-full shadow-xs border-[0.5px] border-divider-subtle'>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -13,7 +13,7 @@ const More: FC<MoreProps> = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center mt-1 h-[18px] text-xs text-gray-400 opacity-0 group-hover:opacity-100'>
|
||||
<div className='mt-1 flex items-center system-xs-regular text-text-quaternary opacity-0 group-hover:opacity-100'>
|
||||
{
|
||||
more && (
|
||||
<>
|
||||
|
||||
@ -5,23 +5,24 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiClipboardLine,
|
||||
RiEditLine,
|
||||
RiReplay15Line,
|
||||
RiThumbDownLine,
|
||||
RiThumbUpLine,
|
||||
} from '@remixicon/react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import RegenerateBtn from '@/app/components/base/regenerate-btn'
|
||||
import cn from '@/utils/classnames'
|
||||
import CopyBtn from '@/app/components/base/copy-btn'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import AudioBtn from '@/app/components/base/audio-btn'
|
||||
import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
|
||||
import {
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
} from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Log from '@/app/components/base/chat/chat/log'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
interface OperationProps {
|
||||
type OperationProps = {
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
@ -60,7 +61,6 @@ const Operation: FC<OperationProps> = ({
|
||||
adminFeedback,
|
||||
agent_thoughts,
|
||||
} = item
|
||||
const hasAnnotation = !!annotation?.id
|
||||
const [localFeedback, setLocalFeedback] = useState(config?.supportAnnotation ? adminFeedback : feedback)
|
||||
|
||||
const content = useMemo(() => {
|
||||
@ -102,121 +102,68 @@ const Operation: FC<OperationProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
'absolute flex justify-end gap-1',
|
||||
hasWorkflowProcess && '-top-3.5 -right-3.5',
|
||||
!positionRight && '-top-3.5 -right-3.5',
|
||||
hasWorkflowProcess && '-bottom-4 right-2',
|
||||
!positionRight && '-bottom-4 right-2',
|
||||
!hasWorkflowProcess && positionRight && '!top-[9px]',
|
||||
)}
|
||||
style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
|
||||
>
|
||||
{!isOpeningStatement && (
|
||||
<CopyBtn
|
||||
value={content}
|
||||
className='hidden group-hover:block'
|
||||
/>
|
||||
{showPromptLog && (
|
||||
<div className='hidden group-hover:block'>
|
||||
<Log logItem={item} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isOpeningStatement && (showPromptLog || config?.text_to_speech?.enabled) && (
|
||||
<div className='hidden group-hover:flex items-center w-max h-[28px] p-0.5 rounded-lg bg-white border-[0.5px] border-gray-100 shadow-md shrink-0'>
|
||||
{showPromptLog && (
|
||||
<>
|
||||
<Log logItem={item} />
|
||||
<div className='mx-1 w-[1px] h-[14px] bg-gray-200' />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isOpeningStatement && (
|
||||
<div className='hidden group-hover:flex ml-1 items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
|
||||
{(config?.text_to_speech?.enabled) && (
|
||||
<>
|
||||
<AudioBtn
|
||||
id={id}
|
||||
value={content}
|
||||
noCache={false}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
className='hidden group-hover:block'
|
||||
/>
|
||||
</>
|
||||
<NewAudioButton
|
||||
id={id}
|
||||
value={content}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
/>
|
||||
)}
|
||||
<ActionButton onClick={() => {
|
||||
copy(content)
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
|
||||
}}>
|
||||
<RiClipboardLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
{!noChatInput && (
|
||||
<ActionButton onClick={() => onRegenerate?.(item)}>
|
||||
<RiReplay15Line className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{(config?.supportAnnotation && config.annotation_reply?.enabled) && (
|
||||
<ActionButton onClick={() => setIsShowReplyModal(true)}>
|
||||
<RiEditLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && (
|
||||
<AnnotationCtrlBtn
|
||||
appId={config?.appId || ''}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
className='hidden group-hover:block ml-1 shrink-0'
|
||||
cached={hasAnnotation}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
onRemoved={() => onAnnotationRemoved?.(index)}
|
||||
/>
|
||||
{!isOpeningStatement && config?.supportFeedback && onFeedback && (
|
||||
<div className='hidden group-hover:flex ml-1 items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
|
||||
{!localFeedback?.rating && (
|
||||
<>
|
||||
<ActionButton onClick={() => handleFeedback('like')}>
|
||||
<RiThumbUpLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={() => handleFeedback('dislike')}>
|
||||
<RiThumbDownLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
{localFeedback?.rating === 'like' && (
|
||||
<ActionButton state={ActionButtonState.Active} onClick={() => handleFeedback(null)}>
|
||||
<RiThumbUpLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{localFeedback?.rating === 'dislike' && (
|
||||
<ActionButton state={ActionButtonState.Destructive} onClick={() => handleFeedback(null)}>
|
||||
<RiThumbDownLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
annotation?.id && (
|
||||
<div
|
||||
className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
|
||||
>
|
||||
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
|
||||
<MessageFast className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isOpeningStatement && !noChatInput && <RegenerateBtn className='hidden group-hover:block mr-1' onClick={() => onRegenerate?.(item)} />
|
||||
}
|
||||
{
|
||||
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||
<div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
|
||||
<Tooltip popupContent={t('appDebug.operation.agree')}>
|
||||
<div
|
||||
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||
onClick={() => handleFeedback('like')}
|
||||
>
|
||||
<ThumbsUp className='w-4 h-4' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={t('appDebug.operation.disagree')}
|
||||
>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||
onClick={() => handleFeedback('dislike')}
|
||||
>
|
||||
<ThumbsDown className='w-4 h-4' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||
<Tooltip
|
||||
popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
|
||||
${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
|
||||
${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
|
||||
`}
|
||||
onClick={() => handleFeedback(null)}
|
||||
>
|
||||
{
|
||||
localFeedback.rating === 'like' && (
|
||||
<ThumbsUp className='w-4 h-4' />
|
||||
)
|
||||
}
|
||||
{
|
||||
localFeedback.rating === 'dislike' && (
|
||||
<ThumbsDown className='w-4 h-4' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<EditReplyModal
|
||||
isShow={isShowReplyModal}
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type SuggestedQuestionsProps = {
|
||||
item: ChatItem
|
||||
@ -21,13 +22,14 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
|
||||
return (
|
||||
<div className='flex flex-wrap'>
|
||||
{suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
|
||||
<div
|
||||
<Button
|
||||
key={index}
|
||||
className='mt-1 mr-1 max-w-full last:mr-0 shrink-0 py-[5px] leading-[18px] items-center px-4 rounded-lg border border-gray-200 shadow-xs bg-white text-xs font-medium text-primary-600 cursor-pointer'
|
||||
variant='secondary-accent'
|
||||
className='mt-1 mr-1 max-w-full last:mr-0 shrink-0'
|
||||
onClick={() => onSend?.(question)}
|
||||
>
|
||||
{question}
|
||||
</div>),
|
||||
</Button>),
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
@ -36,19 +35,6 @@ const WorkflowProcessItem = ({
|
||||
const succeeded = data.status === WorkflowRunningStatus.Succeeded
|
||||
const failed = data.status === WorkflowRunningStatus.Failed || data.status === WorkflowRunningStatus.Stopped
|
||||
|
||||
const background = useMemo(() => {
|
||||
if (collapse)
|
||||
return 'linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%)'
|
||||
if (running && !collapse)
|
||||
return 'linear-gradient(180deg, #E1E4EA 0%, #EAECF0 100%)'
|
||||
|
||||
if (succeeded && !collapse)
|
||||
return 'linear-gradient(180deg, #ECFDF3 0%, #F6FEF9 100%)'
|
||||
|
||||
if (failed && !collapse)
|
||||
return 'linear-gradient(180deg, #FEE4E2 0%, #FEF3F2 100%)'
|
||||
}, [running, succeeded, failed, collapse])
|
||||
|
||||
useEffect(() => {
|
||||
setCollapse(!expand)
|
||||
}, [expand])
|
||||
@ -56,12 +42,13 @@ const WorkflowProcessItem = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'-mx-1 px-2.5 rounded-xl border-[0.5px]',
|
||||
collapse ? 'py-[7px] border-components-panel-border' : 'pt-[7px] px-1 pb-1 border-components-panel-border-subtle',
|
||||
'-mx-1 px-2.5 rounded-xl',
|
||||
collapse ? 'py-[7px] border-l-[0.25px] border-components-panel-border' : 'pt-[7px] px-1 pb-1 border-[0.5px] border-components-panel-border-subtle',
|
||||
running && !collapse && 'bg-background-section-burn',
|
||||
succeeded && !collapse && 'bg-state-success-hover',
|
||||
failed && !collapse && 'bg-state-destructive-hover',
|
||||
collapse && 'bg-workflow-process-bg',
|
||||
)}
|
||||
style={{
|
||||
background,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn('flex items-center cursor-pointer', !collapse && 'px-1.5', readonly && 'cursor-default')}
|
||||
@ -85,7 +72,7 @@ const WorkflowProcessItem = ({
|
||||
<div className={cn('system-xs-medium text-text-secondary', !collapse && 'grow')}>
|
||||
{t('workflow.common.workflowProcess')}
|
||||
</div>
|
||||
{!readonly && <RiArrowRightSLine className={`'ml-1 w-4 h-4 text-text-tertiary' ${collapse ? '' : 'rotate-90'}`} />}
|
||||
{!readonly && <RiArrowRightSLine className={cn('ml-1 w-4 h-4 text-text-tertiary', !collapse && 'rotate-90')} />}
|
||||
</div>
|
||||
{
|
||||
!collapse && !readonly && (
|
||||
|
||||
@ -40,6 +40,7 @@ type ChatInputAreaProps = {
|
||||
inputsForm?: InputForm[]
|
||||
theme?: Theme | null
|
||||
isResponding?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
const ChatInputArea = ({
|
||||
showFeatureBar,
|
||||
@ -53,6 +54,7 @@ const ChatInputArea = ({
|
||||
inputsForm = [],
|
||||
theme,
|
||||
isResponding,
|
||||
disabled,
|
||||
}: ChatInputAreaProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
@ -155,6 +157,7 @@ const ChatInputArea = ({
|
||||
className={cn(
|
||||
'relative pb-[9px] bg-components-panel-bg-blur border border-components-chat-input-border rounded-xl shadow-md z-10',
|
||||
isDragActive && 'border border-dashed border-components-option-card-option-selected-border',
|
||||
disabled && 'opacity-50 pointer-events-none border-components-panel-border shadow-none',
|
||||
)}
|
||||
>
|
||||
<div className='relative px-[9px] pt-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'>
|
||||
|
||||
@ -77,9 +77,9 @@ const Citation: FC<CitationProps> = ({
|
||||
|
||||
return (
|
||||
<div className='mt-3 -mb-1'>
|
||||
<div className='flex items-center mb-2 text-xs font-medium text-gray-500'>
|
||||
<div className='flex items-center mb-2 system-xs-medium text-text-tertiary'>
|
||||
{t('common.chat.citation.title')}
|
||||
<div className='grow ml-2 h-[1px] bg-black/5' />
|
||||
<div className='grow ml-2 h-[1px] bg-divider-regular' />
|
||||
</div>
|
||||
<div className='relative flex flex-wrap'>
|
||||
{
|
||||
@ -87,7 +87,7 @@ const Citation: FC<CitationProps> = ({
|
||||
<div
|
||||
key={index}
|
||||
className='absolute top-0 left-0 w-auto mr-1 mb-1 pl-7 pr-2 max-w-[240px] h-7 text-xs whitespace-nowrap opacity-0 -z-10'
|
||||
ref={ele => (elesRef.current[index] = ele!)}
|
||||
ref={(ele: any) => (elesRef.current[index] = ele!)}
|
||||
>
|
||||
{res.documentName}
|
||||
</div>
|
||||
@ -106,13 +106,13 @@ const Citation: FC<CitationProps> = ({
|
||||
{
|
||||
limitNumberInOneLine < resourcesLength && (
|
||||
<div
|
||||
className='flex items-center px-2 h-7 bg-white rounded-lg text-xs font-medium text-gray-500 cursor-pointer'
|
||||
className='flex items-center px-2 h-7 bg-components-panel-bg rounded-lg text-text-tertiary system-xs-medium cursor-pointer'
|
||||
onClick={() => setShowMore(v => !v)}
|
||||
>
|
||||
{
|
||||
!showMore
|
||||
? `+ ${resourcesLength - limitNumberInOneLine}`
|
||||
: <RiArrowDownSLine className='w-4 h-4 text-gray-600 rotate-180' />
|
||||
: <RiArrowDownSLine className='w-4 h-4 text-text-tertiary rotate-180' />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -47,29 +47,29 @@ const Popup: FC<PopupProps> = ({
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<div className='flex items-center px-2 max-w-[240px] h-7 bg-white rounded-lg'>
|
||||
<div className='flex items-center px-2 max-w-[240px] h-7 bg-components-button-secondary-bg rounded-lg'>
|
||||
<FileIcon type={fileType} className='shrink-0 mr-1 w-4 h-4' />
|
||||
<div className='text-xs text-gray-600 truncate'>{data.documentName}</div>
|
||||
<div className='text-xs text-text-tertiary truncate'>{data.documentName}</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 1000 }}>
|
||||
<div className='max-w-[360px] bg-gray-50 rounded-xl shadow-lg'>
|
||||
<div className='max-w-[360px] bg-background-section-burn rounded-xl shadow-lg'>
|
||||
<div className='px-4 pt-3 pb-2'>
|
||||
<div className='flex items-center h-[18px]'>
|
||||
<FileIcon type={fileType} className='shrink-0 mr-1 w-4 h-4' />
|
||||
<div className='text-xs font-medium text-gray-600 truncate'>{data.documentName}</div>
|
||||
<div className='system-xs-medium text-text-tertiary truncate'>{data.documentName}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-4 py-0.5 max-h-[450px] bg-white rounded-lg overflow-y-auto'>
|
||||
<div className='px-4 py-0.5 max-h-[450px] bg-components-panel-bg rounded-lg overflow-y-auto'>
|
||||
<div className='w-full'>
|
||||
{
|
||||
data.sources.map((source, index) => (
|
||||
<Fragment key={index}>
|
||||
<div className='group py-3'>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<div className='flex items-center px-1.5 h-5 border border-gray-200 rounded-md'>
|
||||
<Hash02 className='mr-0.5 w-3 h-3 text-gray-400' />
|
||||
<div className='text-[11px] font-medium text-gray-500'>
|
||||
<div className='flex items-center px-1.5 h-5 border border-divider-subtle rounded-md'>
|
||||
<Hash02 className='mr-0.5 w-3 h-3 text-text-quaternary' />
|
||||
<div className='text-[11px] font-medium text-text-tertiary'>
|
||||
{source.segment_position || index + 1}
|
||||
</div>
|
||||
</div>
|
||||
@ -77,17 +77,17 @@ const Popup: FC<PopupProps> = ({
|
||||
showHitInfo && (
|
||||
<Link
|
||||
href={`/datasets/${source.dataset_id}/documents/${source.document_id}`}
|
||||
className='hidden items-center h-[18px] text-xs text-primary-600 group-hover:flex'>
|
||||
className='hidden items-center h-[18px] text-xs text-text-accent group-hover:flex'>
|
||||
{t('common.chat.citation.linkToDataset')}
|
||||
<ArrowUpRight className='ml-1 w-3 h-3' />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='text-[13px] text-gray-800 break-words'>{source.content}</div>
|
||||
<div className='text-[13px] text-text-secondary break-words'>{source.content}</div>
|
||||
{
|
||||
showHitInfo && (
|
||||
<div className='flex items-center mt-2 text-xs font-medium text-gray-500 flex-wrap'>
|
||||
<div className='flex items-center mt-2 system-xs-medium text-text-quaternary flex-wrap'>
|
||||
<Tooltip
|
||||
text={t('common.chat.citation.characters')}
|
||||
data={source.word_count}
|
||||
@ -114,7 +114,7 @@ const Popup: FC<PopupProps> = ({
|
||||
</div>
|
||||
{
|
||||
index !== data.sources.length - 1 && (
|
||||
<div className='my-1 h-[1px] bg-black/5' />
|
||||
<div className='my-1 h-[1px] bg-divider-regular' />
|
||||
)
|
||||
}
|
||||
</Fragment>
|
||||
|
||||
@ -28,14 +28,14 @@ const ProgressTooltip: FC<ProgressTooltipProps> = ({
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
>
|
||||
<div className='grow flex items-center'>
|
||||
<div className='mr-1 w-16 h-1.5 rounded-[3px] border border-gray-400 overflow-hidden'>
|
||||
<div className='bg-gray-400 h-full' style={{ width: `${data * 100}%` }}></div>
|
||||
<div className='mr-1 w-16 h-1.5 rounded-[3px] border border-components-progress-gray-border overflow-hidden'>
|
||||
<div className='bg-components-progress-gray-progress h-full' style={{ width: `${data * 100}%` }}></div>
|
||||
</div>
|
||||
{data}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 1001 }}>
|
||||
<div className='p-3 bg-white text-xs font-medium text-gray-500 rounded-lg shadow-lg'>
|
||||
<div className='p-3 bg-components-tooltip-bg system-xs-medium text-text-quaternary rounded-lg shadow-lg'>
|
||||
{t('common.chat.citation.hitScore')} {data}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
||||
@ -35,7 +35,7 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 1001 }}>
|
||||
<div className='p-3 bg-white text-xs font-medium text-gray-500 rounded-lg shadow-lg'>
|
||||
<div className='p-3 bg-components-tooltip-bg system-xs-medium text-text-quaternary rounded-lg shadow-lg'>
|
||||
{text} {data}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
||||
@ -397,6 +397,7 @@ export const useChat = (
|
||||
)
|
||||
setSuggestQuestions(data)
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (e) {
|
||||
setSuggestQuestions([])
|
||||
}
|
||||
@ -555,7 +556,7 @@ export const useChat = (
|
||||
if (!item.execution_metadata?.parallel_id)
|
||||
return item.node_id === nodeFinishedData.node_id
|
||||
|
||||
return item.node_id === nodeFinishedData.node_id && (item.execution_metadata?.parallel_id === nodeFinishedData.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
|
||||
|
||||
|
||||
@ -70,6 +70,8 @@ export type ChatProps = {
|
||||
showFileUpload?: boolean
|
||||
onFeatureBarClick?: (state: boolean) => void
|
||||
noSpacing?: boolean
|
||||
inputDisabled?: boolean
|
||||
isMobile?: boolean
|
||||
}
|
||||
|
||||
const Chat: FC<ChatProps> = ({
|
||||
@ -106,6 +108,8 @@ const Chat: FC<ChatProps> = ({
|
||||
showFileUpload,
|
||||
onFeatureBarClick,
|
||||
noSpacing,
|
||||
inputDisabled,
|
||||
isMobile,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
|
||||
@ -273,12 +277,14 @@ const Chat: FC<ChatProps> = ({
|
||||
<TryToAsk
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
onSend={onSend}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!noChatInput && (
|
||||
<ChatInputArea
|
||||
disabled={inputDisabled}
|
||||
showFeatureBar={showFeatureBar}
|
||||
showFileUpload={showFileUpload}
|
||||
featureBarDisabled={isResponding}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import { RiFileList3Line } from '@remixicon/react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
type LogProps = {
|
||||
logItem: IChatItem
|
||||
@ -10,7 +10,6 @@ type LogProps = {
|
||||
const Log: FC<LogProps> = ({
|
||||
logItem,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const setCurrentLogItem = useAppStore(s => s.setCurrentLogItem)
|
||||
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
|
||||
const setShowAgentLogModal = useAppStore(s => s.setShowAgentLogModal)
|
||||
@ -20,7 +19,7 @@ const Log: FC<LogProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='shrink-0 p-1 flex items-center justify-center rounded-[6px] font-medium text-gray-500 hover:bg-gray-50 cursor-pointer hover:text-gray-700'
|
||||
className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
@ -33,8 +32,9 @@ const Log: FC<LogProps> = ({
|
||||
setShowPromptLogModal(true)
|
||||
}}
|
||||
>
|
||||
<File02 className='mr-1 w-4 h-4' />
|
||||
<div className='text-xs leading-4'>{runID ? t('appLog.viewLog') : isAgent ? t('appLog.agentLog') : t('appLog.promptLog')}</div>
|
||||
<ActionButton>
|
||||
<RiFileList3Line className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,46 +2,37 @@ import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { OnSend } from '../types'
|
||||
import { Star04 } from '@/app/components/base/icons/src/vender/solid/shapes'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type TryToAskProps = {
|
||||
suggestedQuestions: string[]
|
||||
onSend: OnSend
|
||||
isMobile?: boolean
|
||||
}
|
||||
const TryToAsk: FC<TryToAskProps> = ({
|
||||
suggestedQuestions,
|
||||
onSend,
|
||||
isMobile,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex items-center mb-2.5 py-2'>
|
||||
<div
|
||||
className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
|
||||
}}
|
||||
/>
|
||||
<div className='shrink-0 flex items-center px-3 text-gray-500'>
|
||||
<Star04 className='mr-1 w-2.5 h-2.5' />
|
||||
<span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span>
|
||||
</div>
|
||||
<div
|
||||
className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
|
||||
}}
|
||||
/>
|
||||
<div className='mb-2 py-2'>
|
||||
<div className={cn('flex items-center justify-between gap-2 mb-2.5', isMobile && 'justify-end')}>
|
||||
<Divider bgStyle='gradient' className='grow h-px rotate-180' />
|
||||
<div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</div>
|
||||
{!isMobile && <Divider bgStyle='gradient' className='grow h-px' />}
|
||||
</div>
|
||||
<div className='flex flex-wrap justify-center'>
|
||||
<div className={cn('flex flex-wrap justify-center', isMobile && 'justify-end')}>
|
||||
{
|
||||
suggestedQuestions.map((suggestQuestion, index) => (
|
||||
<Button
|
||||
size='small'
|
||||
key={index}
|
||||
variant='secondary-accent'
|
||||
className='mb-2 mr-2 last:mr-0'
|
||||
className='mb-1 mr-1 last:mr-0'
|
||||
onClick={() => onSend(suggestQuestion)}
|
||||
>
|
||||
{suggestQuestion}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Chat from '../chat'
|
||||
import type {
|
||||
ChatConfig,
|
||||
@ -9,16 +9,19 @@ import type {
|
||||
import { useChat } from '../chat/hooks'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
|
||||
import { useEmbeddedChatbotContext } from './context'
|
||||
import ConfigPanel from './config-panel'
|
||||
import { isDify } from './utils'
|
||||
import cn from '@/utils/classnames'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import InputsForm from '@/app/components/base/chat/embedded-chatbot/inputs-form'
|
||||
import {
|
||||
fetchSuggestedQuestions,
|
||||
getUrl,
|
||||
stopChatMessageResponding,
|
||||
} from '@/service/share'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
|
||||
import AnswerIcon from '@/app/components/base/answer-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const ChatWrapper = () => {
|
||||
const {
|
||||
@ -29,6 +32,7 @@ const ChatWrapper = () => {
|
||||
currentConversationItem,
|
||||
inputsForms,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationCompleted,
|
||||
isMobile,
|
||||
isInstalledApp,
|
||||
@ -67,6 +71,38 @@ const ChatWrapper = () => {
|
||||
appPrevChatList,
|
||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||
)
|
||||
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
|
||||
const inputDisabled = useMemo(() => {
|
||||
let hasEmptyInput = ''
|
||||
let fileIsUploading = false
|
||||
const requiredVars = inputsForms.filter(({ required }) => required)
|
||||
if (requiredVars.length) {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (fileIsUploading)
|
||||
return
|
||||
|
||||
if (!inputsFormValue?.[variable])
|
||||
hasEmptyInput = label as string
|
||||
|
||||
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
|
||||
const files = inputsFormValue[variable]
|
||||
if (Array.isArray(files))
|
||||
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
|
||||
else
|
||||
fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
|
||||
}
|
||||
})
|
||||
}
|
||||
if (hasEmptyInput)
|
||||
return true
|
||||
|
||||
if (fileIsUploading)
|
||||
return true
|
||||
return false
|
||||
}, [inputsFormValue, inputsForms])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentChatInstanceRef.current)
|
||||
@ -108,26 +144,48 @@ const ChatWrapper = () => {
|
||||
doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||
}, [chatList, doSend])
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (inputsForms.length) {
|
||||
return (
|
||||
<>
|
||||
{!currentConversationId && (
|
||||
<div className={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}>
|
||||
<div className='mb-6' />
|
||||
<ConfigPanel />
|
||||
<div
|
||||
className='my-6 h-[1px]'
|
||||
style={{ background: 'linear-gradient(90deg, rgba(242, 244, 247, 0.00) 0%, #F2F4F7 49.17%, rgba(242, 244, 247, 0.00) 100%)' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const messageList = useMemo(() => {
|
||||
if (currentConversationId)
|
||||
return chatList
|
||||
return chatList.filter(item => !item.isOpeningStatement)
|
||||
}, [chatList, currentConversationId])
|
||||
|
||||
return null
|
||||
}, [currentConversationId, inputsForms, isMobile])
|
||||
const [collapsed, setCollapsed] = useState(!!currentConversationId)
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (!inputsForms.length)
|
||||
return null
|
||||
if (isMobile) {
|
||||
if (!currentConversationId)
|
||||
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
return <div className='mb-4'></div>
|
||||
}
|
||||
else {
|
||||
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
}
|
||||
}, [inputsForms.length, isMobile, currentConversationId, collapsed])
|
||||
|
||||
const welcome = useMemo(() => {
|
||||
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
||||
if (currentConversationId)
|
||||
return null
|
||||
if (!welcomeMessage)
|
||||
return null
|
||||
if (!collapsed && inputsForms.length > 0)
|
||||
return null
|
||||
return (
|
||||
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
|
||||
<AppIcon
|
||||
size='xl'
|
||||
iconType={appData?.site.icon_type}
|
||||
icon={appData?.site.icon}
|
||||
background={appData?.site.icon_background}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
/>
|
||||
<div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
|
||||
</div>
|
||||
)
|
||||
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
|
||||
|
||||
const answerIcon = isDify()
|
||||
? <LogoAvatar className='relative shrink-0' />
|
||||
@ -144,17 +202,22 @@ const ChatWrapper = () => {
|
||||
<Chat
|
||||
appData={appData}
|
||||
config={appConfig}
|
||||
chatList={chatList}
|
||||
chatList={messageList}
|
||||
isResponding={isResponding}
|
||||
chatContainerInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
|
||||
chatFooterClassName='pb-4'
|
||||
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
|
||||
chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')}
|
||||
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-2')}
|
||||
onSend={doSend}
|
||||
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
|
||||
inputsForm={inputsForms}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
chatNode={chatNode}
|
||||
chatNode={
|
||||
<>
|
||||
{chatNode}
|
||||
{welcome}
|
||||
</>
|
||||
}
|
||||
allToolIcons={appMeta?.tool_icons || {}}
|
||||
onFeedback={handleFeedback}
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
@ -162,6 +225,8 @@ const ChatWrapper = () => {
|
||||
hideProcessDetail
|
||||
themeBuilder={themeBuilder}
|
||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
||||
inputDisabled={inputDisabled}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
interface InputProps {
|
||||
form: any
|
||||
value: string
|
||||
onChange: (variable: string, value: string) => void
|
||||
}
|
||||
const FormInput: FC<InputProps> = ({
|
||||
form,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
required,
|
||||
max_length,
|
||||
variable,
|
||||
} = form
|
||||
|
||||
if (type === 'paragraph') {
|
||||
return (
|
||||
<Textarea
|
||||
value={value}
|
||||
className='resize-none'
|
||||
onChange={e => onChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
className='grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none'
|
||||
value={value || ''}
|
||||
maxLength={max_length}
|
||||
onChange={e => onChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(FormInput)
|
||||
@ -1,129 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEmbeddedChatbotContext } from '../context'
|
||||
import Input from './form-input'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
|
||||
const Form = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appParams,
|
||||
inputsForms,
|
||||
newConversationInputs,
|
||||
newConversationInputsRef,
|
||||
handleNewConversationInputsChange,
|
||||
isMobile,
|
||||
} = useEmbeddedChatbotContext()
|
||||
|
||||
const handleFormChange = useCallback((variable: string, value: any) => {
|
||||
handleNewConversationInputsChange({
|
||||
...newConversationInputsRef.current,
|
||||
[variable]: value,
|
||||
})
|
||||
}, [newConversationInputsRef, handleNewConversationInputsChange])
|
||||
|
||||
const renderField = (form: any) => {
|
||||
const {
|
||||
label,
|
||||
required,
|
||||
variable,
|
||||
options,
|
||||
} = form
|
||||
|
||||
if (form.type === 'text-input' || form.type === 'paragraph') {
|
||||
return (
|
||||
<Input
|
||||
form={form}
|
||||
value={newConversationInputs[variable]}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (form.type === 'number') {
|
||||
return (
|
||||
<input
|
||||
className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
|
||||
type="number"
|
||||
value={newConversationInputs[variable] || ''}
|
||||
onChange={e => handleFormChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (form.type === 'number') {
|
||||
return (
|
||||
<input
|
||||
className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
|
||||
type="number"
|
||||
value={newConversationInputs[variable] || ''}
|
||||
onChange={e => handleFormChange(variable, e.target.value)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (form.type === InputVarType.singleFile) {
|
||||
return (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={newConversationInputs[variable] ? [newConversationInputs[variable]] : []}
|
||||
onChange={files => handleFormChange(variable, files[0])}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: 1,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (form.type === InputVarType.multiFiles) {
|
||||
return (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={newConversationInputs[variable]}
|
||||
onChange={files => handleFormChange(variable, files)}
|
||||
fileConfig={{
|
||||
allowed_file_types: form.allowed_file_types,
|
||||
allowed_file_extensions: form.allowed_file_extensions,
|
||||
allowed_file_upload_methods: form.allowed_file_upload_methods,
|
||||
number_limits: form.max_length,
|
||||
fileUploadConfig: (appParams as any).system_parameters,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalSelect
|
||||
popupClassName='w-[200px]'
|
||||
value={newConversationInputs[variable]}
|
||||
items={options.map((option: string) => ({ value: option, name: option }))}
|
||||
onSelect={item => handleFormChange(variable, item.value as string)}
|
||||
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (!inputsForms.length)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-4 py-2'>
|
||||
{
|
||||
inputsForms.map(form => (
|
||||
<div
|
||||
key={form.variable}
|
||||
className={`flex mb-3 last-of-type:mb-0 text-sm text-gray-900 ${isMobile && '!flex-wrap'}`}
|
||||
>
|
||||
<div className={`shrink-0 mr-2 py-2 w-[128px] ${isMobile && '!w-full'}`}>{form.label}</div>
|
||||
{renderField(form)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@ -1,180 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEmbeddedChatbotContext } from '../context'
|
||||
import { useThemeContext } from '../theme/theme-context'
|
||||
import { CssTransform } from '../theme/utils'
|
||||
import Form from './form'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Edit02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Star06 } from '@/app/components/base/icons/src/vender/solid/shapes'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
|
||||
const ConfigPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appData,
|
||||
inputsForms,
|
||||
handleStartChat,
|
||||
showConfigPanelBeforeChat,
|
||||
isMobile,
|
||||
} = useEmbeddedChatbotContext()
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
const customConfig = appData?.custom_config
|
||||
const site = appData?.site
|
||||
const themeBuilder = useThemeContext()
|
||||
|
||||
return (
|
||||
<div className='flex flex-col max-h-[80%] w-full max-w-[720px]'>
|
||||
<div
|
||||
className={cn(
|
||||
'grow rounded-xl overflow-y-auto',
|
||||
showConfigPanelBeforeChat && 'border-[0.5px] border-gray-100 shadow-lg',
|
||||
!showConfigPanelBeforeChat && collapsed && 'border border-indigo-100',
|
||||
!showConfigPanelBeforeChat && !collapsed && 'border-[0.5px] border-gray-100 shadow-lg',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={CssTransform(themeBuilder.theme?.roundedBackgroundColorStyle ?? '')}
|
||||
className={`
|
||||
flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25
|
||||
${isMobile && '!px-4 !py-3'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
showConfigPanelBeforeChat && (
|
||||
<>
|
||||
<div className='flex items-center h-8 text-2xl font-semibold text-gray-800'>
|
||||
<AppIcon
|
||||
iconType={appData?.site.icon_type}
|
||||
icon={appData?.site.icon}
|
||||
imageUrl={appData?.site.icon_url}
|
||||
background='transparent'
|
||||
size='small'
|
||||
className="mr-2"
|
||||
/>
|
||||
{appData?.site.title}
|
||||
</div>
|
||||
{
|
||||
appData?.site.description && (
|
||||
<div className='mt-2 w-full text-sm text-gray-500'>
|
||||
{appData?.site.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!showConfigPanelBeforeChat && collapsed && (
|
||||
<>
|
||||
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
|
||||
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
|
||||
{t('share.chat.configStatusDes')}
|
||||
</div>
|
||||
<Button
|
||||
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
|
||||
variant='secondary-accent'
|
||||
size='small'
|
||||
className='shrink-0 text-white'
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
<Edit02 className='mr-1 w-3 h-3' />
|
||||
{t('common.operation.edit')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!showConfigPanelBeforeChat && !collapsed && (
|
||||
<>
|
||||
<Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
|
||||
<div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
|
||||
{t('share.chat.privatePromptConfigTitle')}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!collapsed && !showConfigPanelBeforeChat && (
|
||||
<div className='p-6 rounded-b-xl'>
|
||||
<Form />
|
||||
<div className={cn('pl-[136px] flex items-center', isMobile && '!pl-0')}>
|
||||
<Button
|
||||
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
|
||||
variant='primary'
|
||||
className='mr-2'
|
||||
onClick={() => {
|
||||
setCollapsed(true)
|
||||
handleStartChat()
|
||||
}}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setCollapsed(true)}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
showConfigPanelBeforeChat && (
|
||||
<div className='p-6 rounded-b-xl'>
|
||||
<Form />
|
||||
<Button
|
||||
styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
|
||||
className={cn(inputsForms.length && !isMobile && 'ml-[136px]')}
|
||||
variant='primary'
|
||||
size='large'
|
||||
onClick={handleStartChat}
|
||||
>
|
||||
<MessageDotsCircle className='mr-2 w-4 h-4 text-white' />
|
||||
{t('share.chat.startChat')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
showConfigPanelBeforeChat && (site || customConfig) && (
|
||||
<div className='mt-4 flex flex-wrap justify-between items-center py-2 text-xs text-gray-400'>
|
||||
{site?.privacy_policy
|
||||
? <div className={cn(isMobile && 'mb-2 w-full text-center')}>{t('share.chat.privacyPolicyLeft')}
|
||||
<a
|
||||
className='text-gray-500 px-1'
|
||||
href={site?.privacy_policy}
|
||||
target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
|
||||
{t('share.chat.privacyPolicyRight')}
|
||||
</div>
|
||||
: <div>
|
||||
</div>}
|
||||
{
|
||||
customConfig?.remove_webapp_brand
|
||||
? null
|
||||
: (
|
||||
<div className={cn('flex items-center justify-end', isMobile && 'w-full')}>
|
||||
<div className='flex items-center pr-3 space-x-3'>
|
||||
<span className='uppercase'>{t('share.chat.poweredBy')}</span>
|
||||
{
|
||||
customConfig?.replace_webapp_logo
|
||||
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
|
||||
: <LogoSite className='!h-5' />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfigPanel
|
||||
@ -15,7 +15,7 @@ import type {
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
|
||||
export interface EmbeddedChatbotContextValue {
|
||||
export type EmbeddedChatbotContextValue = {
|
||||
appInfoError?: any
|
||||
appInfoLoading?: boolean
|
||||
appMeta?: AppMeta
|
||||
@ -27,13 +27,12 @@ export interface EmbeddedChatbotContextValue {
|
||||
appPrevChatList: ChatItem[]
|
||||
pinnedConversationList: AppConversationData['data']
|
||||
conversationList: AppConversationData['data']
|
||||
showConfigPanelBeforeChat: boolean
|
||||
newConversationInputs: Record<string, any>
|
||||
newConversationInputsRef: RefObject<Record<string, any>>
|
||||
handleNewConversationInputsChange: (v: Record<string, any>) => void
|
||||
inputsForms: any[]
|
||||
handleNewConversation: () => void
|
||||
handleStartChat: () => void
|
||||
handleStartChat: (callback?: any) => void
|
||||
handleChangeConversation: (conversationId: string) => void
|
||||
handleNewConversationCompleted: (newConversationId: string) => void
|
||||
chatShouldReloadKey: string
|
||||
@ -50,7 +49,6 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
|
||||
appPrevChatList: [],
|
||||
pinnedConversationList: [],
|
||||
conversationList: [],
|
||||
showConfigPanelBeforeChat: false,
|
||||
newConversationInputs: {},
|
||||
newConversationInputsRef: { current: {} },
|
||||
handleNewConversationInputsChange: () => {},
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { RiRefreshLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Theme } from './theme/theme-context'
|
||||
import { CssTransform } from './theme/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
export type IHeaderProps = {
|
||||
isMobile?: boolean
|
||||
customerIcon?: React.ReactNode
|
||||
title: string
|
||||
theme?: Theme
|
||||
onCreateNewChat?: () => void
|
||||
}
|
||||
const Header: FC<IHeaderProps> = ({
|
||||
isMobile,
|
||||
customerIcon,
|
||||
title,
|
||||
theme,
|
||||
onCreateNewChat,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
if (!isMobile)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
shrink-0 flex items-center justify-between h-14 px-4
|
||||
`}
|
||||
style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) }
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{customerIcon}
|
||||
<div
|
||||
className={'text-sm font-bold text-white'}
|
||||
style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<div className='flex cursor-pointer hover:rounded-lg hover:bg-black/5 w-8 h-8 items-center justify-center' onClick={() => {
|
||||
onCreateNewChat?.()
|
||||
}}>
|
||||
<RiRefreshLine className="h-4 w-4 text-sm font-bold text-white" color={theme?.colorPathOnHeader}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
109
web/app/components/base/chat/embedded-chatbot/header/index.tsx
Normal file
109
web/app/components/base/chat/embedded-chatbot/header/index.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { RiResetLeftLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Theme } from '../theme/theme-context'
|
||||
import { CssTransform } from '../theme/utils'
|
||||
import {
|
||||
useEmbeddedChatbotContext,
|
||||
} from '../context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type IHeaderProps = {
|
||||
isMobile?: boolean
|
||||
customerIcon?: React.ReactNode
|
||||
title: string
|
||||
theme?: Theme
|
||||
onCreateNewChat?: () => void
|
||||
}
|
||||
const Header: FC<IHeaderProps> = ({
|
||||
isMobile,
|
||||
customerIcon,
|
||||
title,
|
||||
theme,
|
||||
onCreateNewChat,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appData,
|
||||
currentConversationId,
|
||||
inputsForms,
|
||||
} = useEmbeddedChatbotContext()
|
||||
if (!isMobile) {
|
||||
return (
|
||||
<div className='shrink-0 h-14 p-3 flex items-center justify-end'>
|
||||
<div className='flex items-center gap-1'>
|
||||
{/* powered by */}
|
||||
<div className='shrink-0'>
|
||||
{!appData?.custom_config?.remove_webapp_brand && (
|
||||
<div className={cn(
|
||||
'shrink-0 px-2 flex items-center gap-1.5',
|
||||
)}>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
|
||||
{appData?.custom_config?.replace_webapp_logo && (
|
||||
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
|
||||
)}
|
||||
{!appData?.custom_config?.replace_webapp_logo && (
|
||||
<LogoSite className='!h-5' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{currentConversationId && (
|
||||
<Divider type='vertical' className='h-3.5' />
|
||||
)}
|
||||
{currentConversationId && (
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<ActionButton size='l' onClick={onCreateNewChat}>
|
||||
<RiResetLeftLine className='w-[18px] h-[18px]' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentConversationId && inputsForms.length > 0 && (
|
||||
<ViewFormDropdown />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('shrink-0 flex items-center justify-between h-14 px-3 rounded-t-2xl')}
|
||||
style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) }
|
||||
>
|
||||
<div className="grow flex items-center space-x-3">
|
||||
{customerIcon}
|
||||
<div
|
||||
className='system-md-semibold truncate'
|
||||
style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
{currentConversationId && (
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<ActionButton size='l' onClick={onCreateNewChat}>
|
||||
<RiResetLeftLine className={cn('w-[18px] h-[18px]', theme?.colorPathOnHeader)} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentConversationId && inputsForms.length > 0 && (
|
||||
<ViewFormDropdown iconColor={theme?.colorPathOnHeader} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user