Feat: add agent share team viewer (#6222)

### What problem does this PR solve?
Allow member view agent  
#  Canvas editor

![image](https://github.com/user-attachments/assets/042af36d-5fd1-43e2-acf7-05869220a1c1)
# List agent

![image](https://github.com/user-attachments/assets/8b9c7376-780b-47ff-8f5c-6c0e7358158d)
# Setting 

![image](https://github.com/user-attachments/assets/6cb7d12a-7a66-4dd7-9acc-5b53ff79a10a)
 
_Briefly describe what this PR aims to solve. Include background context
that will help reviewers understand the purpose of the PR._

### Type of change

- [ ] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
This commit is contained in:
so95 2025-03-19 18:04:13 +07:00 committed by GitHub
parent d17ec26c56
commit 344727f9ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1665 additions and 1164 deletions

View File

@ -18,6 +18,7 @@ import traceback
from flask import request, Response
from flask_login import login_required, current_user
from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService
from api.db.services.user_service import TenantService
from api.db.services.user_canvas_version import UserCanvasVersionService
from api.settings import RetCode
from api.utils import get_uuid
@ -25,6 +26,7 @@ from api.utils.api_utils import get_json_result, server_error_response, validate
from agent.canvas import Canvas
from peewee import MySQLDatabase, PostgresqlDatabase
from api.db.db_models import APIToken
import logging
import time
@manager.route('/templates', methods=['GET']) # noqa: F821
@ -86,10 +88,11 @@ def save():
@manager.route('/get/<canvas_id>', methods=['GET']) # noqa: F821
@login_required
def get(canvas_id):
e, c = UserCanvasService.get_by_id(canvas_id)
e, c = UserCanvasService.get_by_tenant_id(canvas_id)
logging.info(f"get canvas_id: {canvas_id} c: {c}")
if not e:
return get_data_error_result(message="canvas not found.")
return get_json_result(data=c.to_dict())
return get_json_result(data=c)
@manager.route('/getsse/<canvas_id>', methods=['GET']) # type: ignore # noqa: F821
def getsse(canvas_id):
@ -288,10 +291,6 @@ def test_db_connect():
return get_json_result(data="Database Connection Successful!")
except Exception as e:
return server_error_response(e)
#api get list version dsl of canvas
@manager.route('/getlistversion/<canvas_id>', methods=['GET']) # noqa: F821
@login_required
@ -301,7 +300,6 @@ def getlistversion(canvas_id):
return get_json_result(data=list)
except Exception as e:
return get_data_error_result(message=f"Error getting history files: {e}")
#api get version dsl of canvas
@manager.route('/getversion/<version_id>', methods=['GET']) # noqa: F821
@login_required
@ -313,3 +311,42 @@ def getversion( version_id):
return get_json_result(data=version.to_dict())
except Exception as e:
return get_json_result(data=f"Error getting history file: {e}")
@manager.route('/listteam', methods=['GET']) # noqa: F821
@login_required
def list_kbs():
keywords = request.args.get("keywords", "")
page_number = int(request.args.get("page", 1))
items_per_page = int(request.args.get("page_size", 150))
orderby = request.args.get("orderby", "create_time")
desc = request.args.get("desc", True)
try:
tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
kbs, total = UserCanvasService.get_by_tenant_ids(
[m["tenant_id"] for m in tenants], current_user.id, page_number,
items_per_page, orderby, desc, keywords)
return get_json_result(data={"kbs": kbs, "total": total})
except Exception as e:
return server_error_response(e)
@manager.route('/setting', methods=['POST']) # noqa: F821
@validate_request("id", "title", "permission")
@login_required
def setting():
req = request.json
req["user_id"] = current_user.id
e,flow = UserCanvasService.get_by_id(req["id"])
if not e:
return get_data_error_result(message="canvas not found.")
flow = flow.to_dict()
flow["title"] = req["title"]
if req["description"]:
flow["description"] = req["description"]
if req["permission"]:
flow["permission"] = req["permission"]
if req["avatar"]:
flow["avatar"] = req["avatar"]
if not UserCanvasService.query(user_id=current_user.id, id=req["id"]):
return get_json_result(
data=False, message='Only owner of canvas authorized for this operation.',
code=RetCode.OPERATING_ERROR)
num= UserCanvasService.update_by_id(req["id"], flow)
return get_json_result(data=num)

View File

@ -968,6 +968,12 @@ class UserCanvas(DataBaseModel):
user_id = CharField(max_length=255, null=False, help_text="user_id", index=True)
title = CharField(max_length=255, null=True, help_text="Canvas title")
permission = CharField(
max_length=16,
null=False,
help_text="me|team",
default="me",
index=True)
description = TextField(null=True, help_text="Canvas description")
canvas_type = CharField(max_length=32, null=True, help_text="Canvas type", index=True)
dsl = JSONField(null=True, default={})
@ -1140,3 +1146,11 @@ def migrate_db():
)
except Exception:
pass
try:
migrate(
migrator.add_column("user_canvas", "permission",
CharField(max_length=16, null=False, help_text="me|team", default="me", index=True))
)
except Exception:
pass

View File

@ -18,12 +18,13 @@ import time
import traceback
from uuid import uuid4
from agent.canvas import Canvas
from api.db.db_models import DB, CanvasTemplate, UserCanvas, API4Conversation
from api.db import TenantPermission
from api.db.db_models import DB, CanvasTemplate, User, UserCanvas, API4Conversation
from api.db.services.api_service import API4ConversationService
from api.db.services.common_service import CommonService
from api.db.services.conversation_service import structure_answer
from api.utils import get_uuid
from peewee import fn
class CanvasTemplateService(CommonService):
model = CanvasTemplate
@ -51,6 +52,73 @@ class UserCanvasService(CommonService):
return list(agents.dicts())
@classmethod
@DB.connection_context()
def get_by_tenant_id(cls, pid):
try:
fields = [
cls.model.id,
cls.model.avatar,
cls.model.title,
cls.model.dsl,
cls.model.description,
cls.model.permission,
cls.model.update_time,
cls.model.user_id,
cls.model.create_time,
cls.model.create_date,
cls.model.update_date,
User.nickname,
User.avatar.alias('tenant_avatar'),
]
angents = cls.model.select(*fields) \
.join(User, on=(cls.model.user_id == User.id)) \
.where(cls.model.id == pid)
# obj = cls.model.query(id=pid)[0]
return True, angents.dicts()[0]
except Exception as e:
print(e)
return False, None
@classmethod
@DB.connection_context()
def get_by_tenant_ids(cls, joined_tenant_ids, user_id,
page_number, items_per_page,
orderby, desc, keywords,
):
fields = [
cls.model.id,
cls.model.avatar,
cls.model.title,
cls.model.dsl,
cls.model.description,
cls.model.permission,
User.nickname,
User.avatar.alias('tenant_avatar'),
cls.model.update_time
]
if keywords:
angents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where(
((cls.model.user_id.in_(joined_tenant_ids) & (cls.model.permission ==
TenantPermission.TEAM.value)) | (
cls.model.user_id == user_id)),
(fn.LOWER(cls.model.title).contains(keywords.lower()))
)
else:
angents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where(
((cls.model.user_id.in_(joined_tenant_ids) & (cls.model.permission ==
TenantPermission.TEAM.value)) | (
cls.model.user_id == user_id))
)
if desc:
angents = angents.order_by(cls.model.getter_by(orderby).desc())
else:
angents = angents.order_by(cls.model.getter_by(orderby).asc())
count = angents.count()
angents = angents.paginate(page_number, items_per_page)
return list(angents.dicts()), count
def completion(tenant_id, agent_id, question, session_id=None, stream=True, **kwargs):
e, cvs = UserCanvasService.get_by_id(agent_id)

View File

@ -171,6 +171,27 @@ export const useFetchFlow = (): {
return { data, loading, refetch };
};
export const useSettingFlow = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['SettingFlow'],
mutationFn: async (params: any) => {
const ret = await flowService.settingCanvas(params);
if (ret?.data?.code === 0) {
message.success('success');
} else {
message.error(ret?.data?.data);
}
return ret;
},
});
return { data, loading, settingFlow: mutateAsync };
};
export const useFetchFlowSSE = (): {
data: IFlow;
loading: boolean;
@ -244,7 +265,9 @@ export const useDeleteFlow = () => {
mutationFn: async (canvasIds: string[]) => {
const { data } = await flowService.removeCanvas({ canvasIds });
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchFlowList'] });
queryClient.invalidateQueries({
queryKey: ['infiniteFetchFlowListTeam'],
});
}
return data?.data ?? [];
},

View File

@ -37,6 +37,8 @@ export declare interface IFlow {
update_date: string;
update_time: number;
user_id: string;
permission: string;
nickname: string;
}
export interface IFlowTemplate {

View File

@ -2,7 +2,8 @@ export default {
translation: {
common: {
delete: 'Löschen',
deleteModalTitle: 'Sind Sie sicher, dass Sie diesen Eintrag löschen möchten?',
deleteModalTitle:
'Sind Sie sicher, dass Sie diesen Eintrag löschen möchten?',
ok: 'Ja',
cancel: 'Nein',
total: 'Gesamt',
@ -144,7 +145,8 @@ export default {
'Verwendet ein visuelles Modell für die PDF-Layout-Analyse, um Dokumententitel, Textblöcke, Bilder und Tabellen effektiv zu lokalisieren. Wenn die einfache Option gewählt wird, wird nur der reine Text im PDF abgerufen. Bitte beachten Sie, dass diese Option derzeit NUR für PDF-Dokumente funktioniert.',
taskPageSize: 'Aufgabenseitengröße',
taskPageSizeMessage: 'Bitte geben Sie die Größe der Aufgabenseite ein!',
taskPageSizeTip: 'Während der Layouterkennung wird eine PDF-Datei in Chunks aufgeteilt und parallel verarbeitet, um die Verarbeitungsgeschwindigkeit zu erhöhen. Dieser Parameter legt die Größe jedes Chunks fest. Eine größere Chunk-Größe verringert die Wahrscheinlichkeit, dass fortlaufender Text zwischen den Seiten aufgeteilt wird.',
taskPageSizeTip:
'Während der Layouterkennung wird eine PDF-Datei in Chunks aufgeteilt und parallel verarbeitet, um die Verarbeitungsgeschwindigkeit zu erhöhen. Dieser Parameter legt die Größe jedes Chunks fest. Eine größere Chunk-Größe verringert die Wahrscheinlichkeit, dass fortlaufender Text zwischen den Seiten aufgeteilt wird.',
addPage: 'Seite hinzufügen',
greaterThan: 'Der aktuelle Wert muss größer als "bis" sein!',
greaterThanPrevious:
@ -159,18 +161,22 @@ export default {
cancel: 'Abbrechen',
rerankModel: 'Neuordnungsmodell',
rerankPlaceholder: 'Bitte auswählen',
rerankTip: 'Wenn leer gelassen, verwendet RAGFlow eine Kombination aus gewichteter Schlüsselwortähnlichkeit und gewichteter Vektorkosinus-Ähnlichkeit; wenn ein Neuordnungsmodell ausgewählt wird, ersetzt eine gewichtete Neuordnungsbewertung die gewichtete Vektorkosinus-Ähnlichkeit. Bitte beachten Sie, dass die Verwendung eines Neuordnungsmodells die Antwortzeit des Systems erheblich erhöht.',
rerankTip:
'Wenn leer gelassen, verwendet RAGFlow eine Kombination aus gewichteter Schlüsselwortähnlichkeit und gewichteter Vektorkosinus-Ähnlichkeit; wenn ein Neuordnungsmodell ausgewählt wird, ersetzt eine gewichtete Neuordnungsbewertung die gewichtete Vektorkosinus-Ähnlichkeit. Bitte beachten Sie, dass die Verwendung eines Neuordnungsmodells die Antwortzeit des Systems erheblich erhöht.',
topK: 'Top-K',
topKTip: 'K Chunks werden in das Neuordnungsmodell eingespeist.',
delimiter: 'Trennzeichen',
delimiterTip:
'Ein Trennzeichen oder Separator kann aus einem oder mehreren Sonderzeichen bestehen. Bei mehreren Zeichen stellen Sie sicher, dass sie in Backticks (` `) eingeschlossen sind. Wenn Sie beispielsweise Ihre Trennzeichen so konfigurieren: \n`##`;, dann werden Ihre Texte an Zeilenumbrüchen, doppelten Rautenzeichen (##) oder Semikolons getrennt.',
html4excel: 'Excel zu HTML',
html4excelTip: 'Wenn aktiviert, wird die Tabelle in HTML-Tabellen umgewandelt, mit maximal 256 Zeilen pro Tabelle. Andernfalls wird sie in Schlüssel-Wert-Paare nach Zeilen umgewandelt.',
html4excelTip:
'Wenn aktiviert, wird die Tabelle in HTML-Tabellen umgewandelt, mit maximal 256 Zeilen pro Tabelle. Andernfalls wird sie in Schlüssel-Wert-Paare nach Zeilen umgewandelt.',
autoKeywords: 'Auto-Schlüsselwort',
autoKeywordsTip: 'Extrahieren Sie automatisch N Schlüsselwörter für jeden Abschnitt, um deren Ranking in Abfragen mit diesen Schlüsselwörtern zu verbessern. Beachten Sie, dass zusätzliche Tokens vom in den "Systemmodelleinstellungen" angegebenen Chat-Modell verbraucht werden. Sie können die hinzugefügten Schlüsselwörter eines Abschnitts in der Abschnittsliste überprüfen oder aktualisieren.',
autoKeywordsTip:
'Extrahieren Sie automatisch N Schlüsselwörter für jeden Abschnitt, um deren Ranking in Abfragen mit diesen Schlüsselwörtern zu verbessern. Beachten Sie, dass zusätzliche Tokens vom in den "Systemmodelleinstellungen" angegebenen Chat-Modell verbraucht werden. Sie können die hinzugefügten Schlüsselwörter eines Abschnitts in der Abschnittsliste überprüfen oder aktualisieren.',
autoQuestions: 'Auto-Frage',
autoQuestionsTip: 'Extrahiert automatisch N Fragen für jeden Chunk, um deren Ranking für Anfragen mit diesen Fragen zu erhöhen. Sie können die hinzugefügten Fragen für einen Chunk in der Chunk-Liste überprüfen oder aktualisieren. Diese Funktion unterbricht den Chunking-Prozess nicht, wenn ein Fehler auftritt, außer dass sie möglicherweise ein leeres Ergebnis zum ursprünglichen Chunk hinzufügt. Beachten Sie, dass zusätzliche Tokens vom in den "Systemmodelleinstellungen" angegebenen LLM verbraucht werden.',
autoQuestionsTip:
'Extrahiert automatisch N Fragen für jeden Chunk, um deren Ranking für Anfragen mit diesen Fragen zu erhöhen. Sie können die hinzugefügten Fragen für einen Chunk in der Chunk-Liste überprüfen oder aktualisieren. Diese Funktion unterbricht den Chunking-Prozess nicht, wenn ein Fehler auftritt, außer dass sie möglicherweise ein leeres Ergebnis zum ursprünglichen Chunk hinzufügt. Beachten Sie, dass zusätzliche Tokens vom in den "Systemmodelleinstellungen" angegebenen LLM verbraucht werden.',
redo: 'Möchten Sie die vorhandenen {{chunkNum}} Chunks löschen?',
setMetaData: 'Metadaten festlegen',
pleaseInputJson: 'Bitte JSON eingeben',
@ -338,7 +344,8 @@ export default {
maxTokenTip: 'Maximale Token-Anzahl für die Zusammenfassung.',
maxTokenMessage: 'Maximale Token sind erforderlich',
threshold: 'Schwellenwert',
thresholdTip: 'Je größer der Schwellenwert, desto weniger Cluster werden erzeugt.',
thresholdTip:
'Je größer der Schwellenwert, desto weniger Cluster werden erzeugt.',
thresholdMessage: 'Schwellenwert ist erforderlich',
maxCluster: 'Maximale Cluster',
maxClusterTip: 'Maximale Cluster-Anzahl.',
@ -348,7 +355,8 @@ export default {
entityTypes: 'Entitätstypen',
vietnamese: 'Vietnamesisch',
pageRank: 'PageRank',
pageRankTip: 'Dies erhöht den Relevanzwert der Wissensdatenbank. Sein Wert wird dem Relevanzwert aller abgerufenen Chunks aus dieser Wissensdatenbank hinzugefügt. Nützlich, wenn Sie innerhalb mehrerer Wissensdatenbanken suchen und einer bestimmten einen höheren PageRank-Wert zuweisen möchten.',
pageRankTip:
'Dies erhöht den Relevanzwert der Wissensdatenbank. Sein Wert wird dem Relevanzwert aller abgerufenen Chunks aus dieser Wissensdatenbank hinzugefügt. Nützlich, wenn Sie innerhalb mehrerer Wissensdatenbanken suchen und einer bestimmten einen höheren PageRank-Wert zuweisen möchten.',
tagName: 'Tag',
frequency: 'Häufigkeit',
searchTags: 'Tags durchsuchen',
@ -400,7 +408,8 @@ export default {
graph: 'Wissensgraph',
mind: 'Mind Map',
question: 'Frage',
questionTip: 'Wenn vorgegebene Fragen vorhanden sind, basiert das Embedding des Chunks auf diesen.',
questionTip:
'Wenn vorgegebene Fragen vorhanden sind, basiert das Embedding des Chunks auf diesen.',
},
chat: {
newConversation: 'Neue Unterhaltung',
@ -421,24 +430,30 @@ export default {
assistantAvatar: 'Assistentenbild',
language: 'Sprache',
emptyResponse: 'Leere Antwort',
emptyResponseTip: 'Legen Sie dies als Antwort fest, wenn keine Ergebnisse aus den Wissensdatenbanken für Ihre Anfrage abgerufen werden, oder lassen Sie dieses Feld leer, damit das LLM improvisieren kann, wenn nichts gefunden wird.',
emptyResponseMessage: 'Eine leere Antwort wird ausgelöst, wenn nichts Relevantes aus den Wissensdatenbanken abgerufen wird. Sie müssen das Feld "Leere Antwort" löschen, wenn keine Wissensdatenbank ausgewählt ist.',
emptyResponseTip:
'Legen Sie dies als Antwort fest, wenn keine Ergebnisse aus den Wissensdatenbanken für Ihre Anfrage abgerufen werden, oder lassen Sie dieses Feld leer, damit das LLM improvisieren kann, wenn nichts gefunden wird.',
emptyResponseMessage:
'Eine leere Antwort wird ausgelöst, wenn nichts Relevantes aus den Wissensdatenbanken abgerufen wird. Sie müssen das Feld "Leere Antwort" löschen, wenn keine Wissensdatenbank ausgewählt ist.',
setAnOpener: 'Begrüßungstext',
setAnOpenerInitial: 'Hallo! Ich bin Ihr Assistent, was kann ich für Sie tun?',
setAnOpenerInitial:
'Hallo! Ich bin Ihr Assistent, was kann ich für Sie tun?',
setAnOpenerTip: 'Legen Sie einen Begrüßungstext für Benutzer fest.',
knowledgeBases: 'Wissensdatenbanken',
knowledgeBasesMessage: 'Bitte auswählen',
knowledgeBasesTip:
'Wählen Sie die Wissensdatenbanken aus, die mit diesem Chat-Assistenten verknüpft werden sollen.',
system: 'System',
systemInitialValue: 'Sie sind ein intelligenter Assistent. Bitte fassen Sie den Inhalt der Wissensdatenbank zusammen, um die Frage zu beantworten. Bitte listen Sie die Daten in der Wissensdatenbank auf und antworten Sie detailliert. Wenn alle Inhalte der Wissensdatenbank für die Frage irrelevant sind, muss Ihre Antwort den Satz "Die gesuchte Antwort wurde in der Wissensdatenbank nicht gefunden!" enthalten. Antworten müssen den Chat-Verlauf berücksichtigen.\nHier ist die Wissensdatenbank:\n{knowledge}\nDas oben Genannte ist die Wissensdatenbank.',
systemInitialValue:
'Sie sind ein intelligenter Assistent. Bitte fassen Sie den Inhalt der Wissensdatenbank zusammen, um die Frage zu beantworten. Bitte listen Sie die Daten in der Wissensdatenbank auf und antworten Sie detailliert. Wenn alle Inhalte der Wissensdatenbank für die Frage irrelevant sind, muss Ihre Antwort den Satz "Die gesuchte Antwort wurde in der Wissensdatenbank nicht gefunden!" enthalten. Antworten müssen den Chat-Verlauf berücksichtigen.\nHier ist die Wissensdatenbank:\n{knowledge}\nDas oben Genannte ist die Wissensdatenbank.',
systemMessage: 'Bitte eingeben!',
systemTip:
'Ihre Prompts oder Anweisungen für das LLM, einschließlich, aber nicht beschränkt auf seine Rolle, die gewünschte Länge, den Ton und die Sprache seiner Antworten.',
topN: 'Top N',
topNTip: 'Nicht alle Chunks mit einem Ähnlichkeitswert über dem "Ähnlichkeitsschwellenwert" werden an das LLM gesendet. Dies wählt die "Top N" Chunks aus den abgerufenen aus.',
topNTip:
'Nicht alle Chunks mit einem Ähnlichkeitswert über dem "Ähnlichkeitsschwellenwert" werden an das LLM gesendet. Dies wählt die "Top N" Chunks aus den abgerufenen aus.',
variable: 'Variable',
variableTip: 'Variablen können bei der Entwicklung flexiblerer Strategien helfen, insbesondere wenn Sie unsere Chat-Assistenten-Management-APIs verwenden. Diese Variablen werden von "System" als Teil der Prompts für das LLM verwendet. Die Variable {knowledge} ist eine reservierte spezielle Variable, die Ihre ausgewählte(n) Wissensdatenbank(en) repräsentiert, und alle Variablen sollten in geschweifte Klammern {} eingeschlossen sein.',
variableTip:
'Variablen können bei der Entwicklung flexiblerer Strategien helfen, insbesondere wenn Sie unsere Chat-Assistenten-Management-APIs verwenden. Diese Variablen werden von "System" als Teil der Prompts für das LLM verwendet. Die Variable {knowledge} ist eine reservierte spezielle Variable, die Ihre ausgewählte(n) Wissensdatenbank(en) repräsentiert, und alle Variablen sollten in geschweifte Klammern {} eingeschlossen sein.',
add: 'Hinzufügen',
key: 'Schlüssel',
optional: 'Optional',
@ -450,10 +465,12 @@ export default {
improvise: 'Improvisieren',
precise: 'Präzise',
balance: 'Ausgewogen',
freedomTip: 'Eine Abkürzung für die Einstellungen "Temperatur", "Top P", "Präsenzstrafe" und "Häufigkeitsstrafe", die den Freiheitsgrad des Modells angibt. Dieser Parameter hat drei Optionen: Wählen Sie "Improvisieren", um kreativere Antworten zu erzeugen; wählen Sie "Präzise" (Standard), um konservativere Antworten zu erzeugen; "Ausgewogen" ist ein Mittelweg zwischen "Improvisieren" und "Präzise".',
freedomTip:
'Eine Abkürzung für die Einstellungen "Temperatur", "Top P", "Präsenzstrafe" und "Häufigkeitsstrafe", die den Freiheitsgrad des Modells angibt. Dieser Parameter hat drei Optionen: Wählen Sie "Improvisieren", um kreativere Antworten zu erzeugen; wählen Sie "Präzise" (Standard), um konservativere Antworten zu erzeugen; "Ausgewogen" ist ein Mittelweg zwischen "Improvisieren" und "Präzise".',
temperature: 'Temperatur',
temperatureMessage: 'Temperatur ist erforderlich',
temperatureTip: 'Dieser Parameter steuert die Zufälligkeit der Vorhersagen des Modells. Eine niedrigere Temperatur führt zu konservativeren Antworten, während eine höhere Temperatur kreativere und vielfältigere Antworten liefert.',
temperatureTip:
'Dieser Parameter steuert die Zufälligkeit der Vorhersagen des Modells. Eine niedrigere Temperatur führt zu konservativeren Antworten, während eine höhere Temperatur kreativere und vielfältigere Antworten liefert.',
topP: 'Top P',
topPMessage: 'Top P ist erforderlich',
topPTip:
@ -468,13 +485,16 @@ export default {
'Ähnlich wie die Präsenzstrafe reduziert dies die Tendenz des Modells, dieselben Wörter häufig zu wiederholen.',
maxTokens: 'Maximale Tokens',
maxTokensMessage: 'Maximale Tokens sind erforderlich',
maxTokensTip: 'Dies legt die maximale Länge der Ausgabe des Modells fest, gemessen an der Anzahl der Tokens (Wörter oder Wortteile). Standardmäßig 512. Wenn deaktiviert, heben Sie die maximale Token-Begrenzung auf und erlauben dem Modell, die Anzahl der Tokens in seinen Antworten selbst zu bestimmen.',
maxTokensInvalidMessage: 'Bitte geben Sie eine gültige Zahl für Maximale Tokens ein.',
maxTokensTip:
'Dies legt die maximale Länge der Ausgabe des Modells fest, gemessen an der Anzahl der Tokens (Wörter oder Wortteile). Standardmäßig 512. Wenn deaktiviert, heben Sie die maximale Token-Begrenzung auf und erlauben dem Modell, die Anzahl der Tokens in seinen Antworten selbst zu bestimmen.',
maxTokensInvalidMessage:
'Bitte geben Sie eine gültige Zahl für Maximale Tokens ein.',
maxTokensMinMessage: 'Maximale Tokens können nicht weniger als 0 sein.',
quote: 'Zitat anzeigen',
quoteTip: 'Ob der Originaltext als Referenz angezeigt werden soll.',
selfRag: 'Self-RAG',
selfRagTip: 'Bitte beziehen Sie sich auf: https://huggingface.co/papers/2310.11511',
selfRagTip:
'Bitte beziehen Sie sich auf: https://huggingface.co/papers/2310.11511',
overview: 'Chat-ID',
pv: 'Anzahl der Nachrichten',
uv: 'Anzahl aktiver Benutzer',
@ -523,7 +543,8 @@ export default {
useKnowledgeGraphTip:
'Es werden Beschreibungen relevanter Entitäten, Beziehungen und Gemeinschaftsberichte abgerufen, was die Inferenz von mehrschrittigen und komplexen Fragen verbessert.',
keyword: 'Schlüsselwortanalyse',
keywordTip: 'LLM anwenden, um die Fragen des Benutzers zu analysieren und Schlüsselwörter zu extrahieren, die während der Relevanzberechnung hervorgehoben werden.',
keywordTip:
'LLM anwenden, um die Fragen des Benutzers zu analysieren und Schlüsselwörter zu extrahieren, die während der Relevanzberechnung hervorgehoben werden.',
languageTip:
'Ermöglicht die Umformulierung von Sätzen in der angegebenen Sprache oder verwendet standardmäßig die letzte Frage, wenn keine ausgewählt ist.',
avatarHidden: 'Avatar ausblenden',
@ -539,17 +560,21 @@ export default {
},
setting: {
profile: 'Profil',
profileDescription: 'Aktualisieren Sie hier Ihr Foto und Ihre persönlichen Daten.',
profileDescription:
'Aktualisieren Sie hier Ihr Foto und Ihre persönlichen Daten.',
maxTokens: 'Maximale Tokens',
maxTokensMessage: 'Maximale Tokens sind erforderlich',
maxTokensTip: 'Dies legt die maximale Länge der Ausgabe des Modells fest, gemessen an der Anzahl der Tokens (Wörter oder Wortteile). Standardmäßig 512. Wenn deaktiviert, heben Sie die maximale Token-Begrenzung auf und erlauben dem Modell, die Anzahl der Tokens in seinen Antworten selbst zu bestimmen.',
maxTokensInvalidMessage: 'Bitte geben Sie eine gültige Zahl für Maximale Tokens ein.',
maxTokensTip:
'Dies legt die maximale Länge der Ausgabe des Modells fest, gemessen an der Anzahl der Tokens (Wörter oder Wortteile). Standardmäßig 512. Wenn deaktiviert, heben Sie die maximale Token-Begrenzung auf und erlauben dem Modell, die Anzahl der Tokens in seinen Antworten selbst zu bestimmen.',
maxTokensInvalidMessage:
'Bitte geben Sie eine gültige Zahl für Maximale Tokens ein.',
maxTokensMinMessage: 'Maximale Tokens können nicht weniger als 0 sein.',
password: 'Passwort',
passwordDescription:
'Bitte geben Sie Ihr aktuelles Passwort ein, um Ihr Passwort zu ändern.',
model: 'Modellanbieter',
modelDescription: 'Legen Sie hier die Modellparameter und den API-SCHLÜSSEL fest.',
modelDescription:
'Legen Sie hier die Modellparameter und den API-SCHLÜSSEL fest.',
team: 'Team',
system: 'System',
logout: 'Abmelden',
@ -567,7 +592,8 @@ export default {
timezoneMessage: 'Bitte geben Sie Ihre Zeitzone ein!',
timezonePlaceholder: 'Wählen Sie Ihre Zeitzone',
email: 'E-Mail-Adresse',
emailDescription: 'Nach der Registrierung kann die E-Mail nicht mehr geändert werden.',
emailDescription:
'Nach der Registrierung kann die E-Mail nicht mehr geändert werden.',
currentPassword: 'Aktuelles Passwort',
currentPasswordMessage: 'Bitte geben Sie Ihr Passwort ein!',
newPassword: 'Neues Passwort',
@ -606,7 +632,8 @@ export default {
sequence2txtModelTip:
'Das Standard-ASR-Modell, das alle neu erstellten Wissensdatenbanken verwenden werden. Verwenden Sie dieses Modell, um Stimmen in entsprechenden Text zu übersetzen.',
rerankModel: 'Rerank-Modell',
rerankModelTip: 'Das Standard-Rerank-Modell wird verwendet, um Chunks neu zu ordnen, die durch Benutzerfragen abgerufen wurden.',
rerankModelTip:
'Das Standard-Rerank-Modell wird verwendet, um Chunks neu zu ordnen, die durch Benutzerfragen abgerufen wurden.',
ttsModel: 'TTS-Modell',
ttsModelTip:
'Das Standard-TTS-Modell wird verwendet, um auf Anfrage Sprache während Gesprächen zu generieren.',
@ -682,7 +709,8 @@ export default {
'Bitte geben Sie den Google Cloud Dienstkontoschlüssel im base64-Format ein',
addGoogleRegion: 'Google Cloud Region',
GoogleRegionMessage: 'Bitte geben Sie die Google Cloud Region ein',
modelProvidersWarn: 'Bitte fügen Sie zuerst sowohl das Embedding-Modell als auch das LLM in <b>Einstellungen > Modellanbieter</b> hinzu. Legen Sie sie dann in "Systemmodelleinstellungen" fest.',
modelProvidersWarn:
'Bitte fügen Sie zuerst sowohl das Embedding-Modell als auch das LLM in <b>Einstellungen > Modellanbieter</b> hinzu. Legen Sie sie dann in "Systemmodelleinstellungen" fest.',
apiVersion: 'API-Version',
apiVersionMessage: 'Bitte geben Sie die API-Version ein',
add: 'Hinzufügen',
@ -693,9 +721,11 @@ export default {
refuse: 'Ablehnen',
teamMembers: 'Teammitglieder',
joinedTeams: 'Beigetretene Teams',
sureDelete: 'Sind Sie sicher, dass Sie dieses Mitglied entfernen möchten?',
sureDelete:
'Sind Sie sicher, dass Sie dieses Mitglied entfernen möchten?',
quit: 'Verlassen',
sureQuit: 'Sind Sie sicher, dass Sie das Team, dem Sie beigetreten sind, verlassen möchten?',
sureQuit:
'Sind Sie sicher, dass Sie das Team, dem Sie beigetreten sind, verlassen möchten?',
},
message: {
registered: 'Registriert!',
@ -781,18 +811,28 @@ export default {
save: 'Speichern',
title: 'ID:',
beginDescription: 'Hier beginnt der Ablauf.',
answerDescription: 'Eine Komponente, die als Schnittstelle zwischen Mensch und Bot dient, Benutzereingaben empfängt und die Antworten des Agenten anzeigt.',
retrievalDescription: 'Eine Komponente, die Informationen aus bestimmten Wissensdatenbanken (Datensätzen) abruft. Stellen Sie sicher, dass die von Ihnen ausgewählten Wissensdatenbanken dasselbe Embedding-Modell verwenden.',
generateDescription: 'Eine Komponente, die das LLM auffordert, Antworten zu generieren. Stellen Sie sicher, dass der Prompt korrekt eingestellt ist.',
categorizeDescription: 'Eine Komponente, die das LLM verwendet, um Benutzereingaben in vordefinierte Kategorien zu klassifizieren. Stellen Sie sicher, dass Sie für jede Kategorie den Namen, die Beschreibung und Beispiele sowie die entsprechende nächste Komponente angeben.',
relevantDescription: 'Eine Komponente, die das LLM verwendet, um zu beurteilen, ob die vorgelagerte Ausgabe für die neueste Anfrage des Benutzers relevant ist. Stellen Sie sicher, dass Sie die nächste Komponente für jedes Beurteilungsergebnis angeben.',
rewriteQuestionDescription: 'Eine Komponente, die eine Benutzeranfrage aus der Interaktionskomponente basierend auf dem Kontext vorheriger Dialoge umformuliert.',
answerDescription:
'Eine Komponente, die als Schnittstelle zwischen Mensch und Bot dient, Benutzereingaben empfängt und die Antworten des Agenten anzeigt.',
retrievalDescription:
'Eine Komponente, die Informationen aus bestimmten Wissensdatenbanken (Datensätzen) abruft. Stellen Sie sicher, dass die von Ihnen ausgewählten Wissensdatenbanken dasselbe Embedding-Modell verwenden.',
generateDescription:
'Eine Komponente, die das LLM auffordert, Antworten zu generieren. Stellen Sie sicher, dass der Prompt korrekt eingestellt ist.',
categorizeDescription:
'Eine Komponente, die das LLM verwendet, um Benutzereingaben in vordefinierte Kategorien zu klassifizieren. Stellen Sie sicher, dass Sie für jede Kategorie den Namen, die Beschreibung und Beispiele sowie die entsprechende nächste Komponente angeben.',
relevantDescription:
'Eine Komponente, die das LLM verwendet, um zu beurteilen, ob die vorgelagerte Ausgabe für die neueste Anfrage des Benutzers relevant ist. Stellen Sie sicher, dass Sie die nächste Komponente für jedes Beurteilungsergebnis angeben.',
rewriteQuestionDescription:
'Eine Komponente, die eine Benutzeranfrage aus der Interaktionskomponente basierend auf dem Kontext vorheriger Dialoge umformuliert.',
messageDescription:
'Eine Komponente, die eine statische Nachricht sendet. Wenn mehrere Nachrichten bereitgestellt werden, wählt sie zufällig eine zum Senden aus. Stellen Sie sicher, dass ihr nachgelagerter Bereich "Interact" ist, die Schnittstellenkomponente.',
keywordDescription: 'Eine Komponente, die die Top-N-Suchergebnisse aus der Benutzereingabe abruft. Stellen Sie sicher, dass der TopN-Wert vor der Verwendung richtig eingestellt ist.',
switchDescription: 'Eine Komponente, die Bedingungen basierend auf der Ausgabe vorheriger Komponenten auswertet und den Ausführungsfluss entsprechend lenkt. Sie ermöglicht komplexe Verzweigungslogik, indem Fälle definiert und Aktionen für jeden Fall oder Standardaktionen festgelegt werden, wenn keine Bedingungen erfüllt sind.',
wikipediaDescription: 'Eine Komponente, die auf wikipedia.org sucht und mit TopN die Anzahl der Suchergebnisse angibt. Sie ergänzt die vorhandenen Wissensdatenbanken.',
promptText: 'Bitte fassen Sie die folgenden Absätze zusammen. Seien Sie vorsichtig mit den Zahlen, erfinden Sie nichts. Absätze wie folgt:\n{input}\nDas oben ist der Inhalt, den Sie zusammenfassen müssen.',
keywordDescription:
'Eine Komponente, die die Top-N-Suchergebnisse aus der Benutzereingabe abruft. Stellen Sie sicher, dass der TopN-Wert vor der Verwendung richtig eingestellt ist.',
switchDescription:
'Eine Komponente, die Bedingungen basierend auf der Ausgabe vorheriger Komponenten auswertet und den Ausführungsfluss entsprechend lenkt. Sie ermöglicht komplexe Verzweigungslogik, indem Fälle definiert und Aktionen für jeden Fall oder Standardaktionen festgelegt werden, wenn keine Bedingungen erfüllt sind.',
wikipediaDescription:
'Eine Komponente, die auf wikipedia.org sucht und mit TopN die Anzahl der Suchergebnisse angibt. Sie ergänzt die vorhandenen Wissensdatenbanken.',
promptText:
'Bitte fassen Sie die folgenden Absätze zusammen. Seien Sie vorsichtig mit den Zahlen, erfinden Sie nichts. Absätze wie folgt:\n{input}\nDas oben ist der Inhalt, den Sie zusammenfassen müssen.',
createGraph: 'Agenten erstellen',
createFromTemplates: 'Aus Vorlagen erstellen',
retrieval: 'Abruf',
@ -811,14 +851,17 @@ export default {
nameRequiredMsg: 'Name ist erforderlich',
nameRepeatedMsg: 'Der Name darf nicht wiederholt werden',
keywordExtract: 'Schlüsselwort',
keywordExtractDescription: 'Eine Komponente, die Schlüsselwörter aus einer Benutzeranfrage extrahiert, wobei Top N die Anzahl der zu extrahierenden Schlüsselwörter angibt.',
keywordExtractDescription:
'Eine Komponente, die Schlüsselwörter aus einer Benutzeranfrage extrahiert, wobei Top N die Anzahl der zu extrahierenden Schlüsselwörter angibt.',
baidu: 'Baidu',
baiduDescription: 'Eine Komponente, die auf baidu.com sucht und mit TopN die Anzahl der Suchergebnisse angibt. Sie ergänzt die vorhandenen Wissensdatenbanken.',
baiduDescription:
'Eine Komponente, die auf baidu.com sucht und mit TopN die Anzahl der Suchergebnisse angibt. Sie ergänzt die vorhandenen Wissensdatenbanken.',
duckDuckGo: 'DuckDuckGo',
duckDuckGoDescription:
'Eine Komponente, die auf duckduckgo.com sucht und Ihnen ermöglicht, die Anzahl der Suchergebnisse mit TopN anzugeben. Sie ergänzt die vorhandenen Wissensdatenbanken.',
channel: 'Kanal',
channelTip: 'Führt eine Textsuche oder Nachrichtensuche für die Eingabe der Komponente durch',
channelTip:
'Führt eine Textsuche oder Nachrichtensuche für die Eingabe der Komponente durch',
text: 'Text',
news: 'Nachrichten',
messageHistoryWindowSize: 'Nachrichtenfenstergröße',
@ -1102,7 +1145,8 @@ export default {
noteDescription: 'Notiz',
notePlaceholder: 'Bitte geben Sie eine Notiz ein',
invoke: 'Aufrufen',
invokeDescription: 'Eine Komponente, die Remote-Dienste aufrufen kann und dabei die Ausgaben anderer Komponenten oder Konstanten als Eingaben verwendet.',
invokeDescription:
'Eine Komponente, die Remote-Dienste aufrufen kann und dabei die Ausgaben anderer Komponenten oder Konstanten als Eingaben verwendet.',
url: 'URL',
method: 'Methode',
timeout: 'Zeitüberschreitung',
@ -1152,7 +1196,8 @@ export default {
jsonUploadTypeErrorMessage: 'Bitte laden Sie eine JSON-Datei hoch',
jsonUploadContentErrorMessage: 'JSON-Dateifehler',
iteration: 'Iteration',
iterationDescription: 'Diese Komponente teilt zunächst die Eingabe durch "Trennzeichen" in ein Array auf. Führt die gleichen Operationsschritte nacheinander für die Elemente im Array aus, bis alle Ergebnisse ausgegeben sind, was als Aufgaben-Batch-Prozessor verstanden werden kann.\n\nZum Beispiel kann innerhalb des Iterationsknotens für lange Textübersetzungen, wenn der gesamte Inhalt in den LLM-Knoten eingegeben wird, das Limit für eine einzelne Konversation erreicht werden. Der vorgelagerte Knoten kann den langen Text zuerst in mehrere Fragmente aufteilen und mit dem Iterationsknoten zusammenarbeiten, um eine Batch-Übersetzung für jedes Fragment durchzuführen, um zu vermeiden, dass das LLM-Nachrichtenlimit für eine einzelne Konversation erreicht wird.',
iterationDescription:
'Diese Komponente teilt zunächst die Eingabe durch "Trennzeichen" in ein Array auf. Führt die gleichen Operationsschritte nacheinander für die Elemente im Array aus, bis alle Ergebnisse ausgegeben sind, was als Aufgaben-Batch-Prozessor verstanden werden kann.\n\nZum Beispiel kann innerhalb des Iterationsknotens für lange Textübersetzungen, wenn der gesamte Inhalt in den LLM-Knoten eingegeben wird, das Limit für eine einzelne Konversation erreicht werden. Der vorgelagerte Knoten kann den langen Text zuerst in mehrere Fragmente aufteilen und mit dem Iterationsknoten zusammenarbeiten, um eine Batch-Übersetzung für jedes Fragment durchzuführen, um zu vermeiden, dass das LLM-Nachrichtenlimit für eine einzelne Konversation erreicht wird.',
delimiterTip:
'Dieses Trennzeichen wird verwendet, um den Eingabetext in mehrere Textstücke aufzuteilen, von denen jedes als Eingabeelement jeder Iteration ausgeführt wird.',
delimiterOptions: {

View File

@ -1192,6 +1192,15 @@ This delimiter is used to split the input text into several text pieces echo of
addCategory: 'Add category',
categoryName: 'Category name',
nextStep: 'Next step',
variableExtractDescription:
'Extract user information into global variable throughout the conversation',
variableExtract: 'Variables',
variables: 'Variables',
variablesTip: `Set the clear json key variable with a value of empty. e.g.
{
"UserCode":"",
"NumberPhone":""
}`,
datatype: 'MINE type of the HTTP request',
insertVariableTip: `Enter / Insert variables`,
historyversion: 'History version',
@ -1204,7 +1213,17 @@ This delimiter is used to split the input text into several text pieces echo of
version: 'Version',
select: 'No version selected',
},
setting: 'Setting',
settings: {
upload: 'Upload',
photo: 'Photo',
permissions: 'Permission',
permissionsTip: 'You can set the permissions of the team members here.',
me: 'me',
team: 'Team',
},
noMoreData: 'No more data',
searchAgentPlaceholder: 'Search agent',
footer: {
profile: 'All rights reserved @ React',
},
@ -1214,4 +1233,5 @@ This delimiter is used to split the input text into several text pieces echo of
chat: 'chat',
},
},
},
};

View File

@ -722,7 +722,8 @@ export default {
s3: 'S3 上傳',
preview: '預覽',
fileError: '文件錯誤',
uploadLimit: '本地部署的單次上傳檔案總大小上限為 1GB單次批量上傳檔案數不超過 32單個帳戶不限檔案數量。',
uploadLimit:
'本地部署的單次上傳檔案總大小上限為 1GB單次批量上傳檔案數不超過 32單個帳戶不限檔案數量。',
destinationFolder: '目標資料夾',
},
flow: {

View File

@ -0,0 +1,121 @@
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchFlow, useSettingFlow } from '@/hooks/flow-hooks';
import { normFile } from '@/utils/file-util';
import { PlusOutlined } from '@ant-design/icons';
import { Form, Input, Modal, Radio, Upload } from 'antd';
import React, { useCallback, useEffect } from 'react';
export function useFlowSettingModal() {
const [visibleSettingModal, setVisibleSettingMModal] = React.useState(false);
return {
visibleSettingModal,
setVisibleSettingMModal,
};
}
type FlowSettingModalProps = {
visible: boolean;
hideModal: () => void;
id: string;
};
export const FlowSettingModal = ({
hideModal,
visible,
id,
}: FlowSettingModalProps) => {
const { data, refetch } = useFetchFlow();
const [form] = Form.useForm();
const { t } = useTranslate('flow.settings');
const { loading, settingFlow } = useSettingFlow();
// Initialize form with data when it becomes available
useEffect(() => {
if (data) {
form.setFieldsValue({
title: data.title,
description: data.description,
permission: data.permission,
avatar: data.avatar ? [{ thumbUrl: data.avatar }] : [],
});
}
}, [data, form]);
const handleSubmit = useCallback(async () => {
if (!id) return;
try {
const { avatar, ...others } = await form.validateFields();
const param = {
...others,
id,
avatar: avatar && avatar.length > 0 ? avatar[0].thumbUrl : '',
};
settingFlow(param);
} catch (error) {
console.error('Validation failed:', error);
}
}, [form, id, settingFlow]);
React.useEffect(() => {
if (!loading && refetch && visible) {
refetch();
}
}, [loading, refetch, visible]);
return (
<Modal
confirmLoading={loading}
title={'Agent Setting'}
open={visible}
onCancel={hideModal}
onOk={handleSubmit}
okText={t('save', { keyPrefix: 'common' })}
cancelText={t('cancel', { keyPrefix: 'common' })}
>
<Form
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
layout="horizontal"
style={{ maxWidth: 600 }}
>
<Form.Item
name="title"
label="Title"
rules={[{ required: true, message: 'Please input a title!' }]}
>
<Input />
</Form.Item>
<Form.Item
name="avatar"
label={t('photo')}
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
listType="picture-card"
maxCount={1}
beforeUpload={() => false}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
>
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>{t('upload')}</div>
</button>
</Upload>
</Form.Item>
<Form.Item name="description" label="Description">
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item
name="permission"
label={t('permissions')}
tooltip={t('permissionsTip')}
rules={[{ required: true }]}
>
<Radio.Group>
<Radio value="me">{t('me')}</Radio>
<Radio value="team">{t('team')}</Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
);
};

View File

@ -1,3 +1,10 @@
.flowHeader {
padding: 10px 20px;
}
.hideRibbon {
display: none !important;
}
.ribbon {
top: 4px;
}

View File

@ -3,10 +3,13 @@ import { useShowEmbedModal } from '@/components/api-service/hooks';
import { SharedFrom } from '@/constants/chat';
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchFlow } from '@/hooks/flow-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { Button, Flex, Space } from 'antd';
import { Badge, Button, Flex, Space } from 'antd';
import classNames from 'classnames';
import { useCallback } from 'react';
import { Link, useParams } from 'umi';
import { FlowSettingModal, useFlowSettingModal } from '../flow-setting';
import {
useGetBeginNodeDataQuery,
useGetBeginNodeDataQueryIsSafe,
@ -32,6 +35,8 @@ interface IProps {
const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
const { saveGraph } = useSaveGraph();
const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
const { data: userInfo } = useFetchUserInfo();
const { data } = useFetchFlow();
const { t } = useTranslate('flow');
const { id } = useParams();
@ -39,6 +44,8 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal();
const { setVisibleSettingMModal, visibleSettingModal } =
useFlowSettingModal();
const isBeginNodeDataQuerySafe = useGetBeginNodeDataQueryIsSafe();
const { setVisibleHistoryVersionModal, visibleHistoryVersionModal } =
useHistoryVersionModal();
@ -55,6 +62,10 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
}
}, [getBeginNodeDataQuery, handleRun, showChatDrawer]);
const showSetting = useCallback(() => {
setVisibleSettingMModal(true);
}, [setVisibleSettingMModal]);
const showListVersion = useCallback(() => {
setVisibleHistoryVersionModal(true);
}, [setVisibleHistoryVersionModal]);
@ -66,7 +77,15 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
gap={'large'}
className={styles.flowHeader}
>
<Space size={'large'}>
<Badge.Ribbon
text={data?.nickname}
style={{ marginRight: -data?.nickname?.length * 5 }}
color={userInfo?.nickname === data?.nickname ? '#1677ff' : 'pink'}
className={classNames(styles.ribbon, {
[styles.hideRibbon]: data.permission !== 'team',
})}
>
<Space className={styles.headerTitle} size={'large'}>
<Link to={`/flow`}>
<ArrowLeftOutlined />
</Link>
@ -77,20 +96,37 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
</span>
</div>
</Space>
</Badge.Ribbon>
<Space size={'large'}>
<Button onClick={handleRunAgent}>
<Button
disabled={userInfo.nickname !== data.nickname}
onClick={handleRunAgent}
>
<b>{t('run')}</b>
</Button>
<Button type="primary" onClick={() => saveGraph()}>
<Button
disabled={userInfo.nickname !== data.nickname}
type="primary"
onClick={() => saveGraph()}
>
<b>{t('save')}</b>
</Button>
<Button
type="primary"
onClick={handleShowEmbedModal}
disabled={!isBeginNodeDataQuerySafe}
disabled={
!isBeginNodeDataQuerySafe || userInfo.nickname !== data.nickname
}
>
<b>{t('embedIntoSite', { keyPrefix: 'common' })}</b>
</Button>
<Button
disabled={userInfo.nickname !== data.nickname}
type="primary"
onClick={showSetting}
>
<b>{t('setting')}</b>
</Button>
<Button type="primary" onClick={showListVersion}>
<b>{t('historyversion')}</b>
</Button>
@ -106,6 +142,13 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
isAgent
></EmbedModal>
)}
{visibleSettingModal && (
<FlowSettingModal
id={id || ''}
visible={visibleSettingModal}
hideModal={() => setVisibleSettingMModal(false)}
></FlowSettingModal>
)}
{visibleHistoryVersionModal && (
<HistoryVersionModal
id={id || ''}

View File

@ -74,3 +74,11 @@
vertical-align: middle;
}
}
.hideRibbon {
display: none !important;
}
.ribbon {
top: 4px;
}

View File

@ -1,22 +1,26 @@
import { formatDate } from '@/utils/date';
import { CalendarOutlined } from '@ant-design/icons';
import { Card, Typography } from 'antd';
import { Badge, Card, Typography } from 'antd';
import { useNavigate } from 'umi';
import OperateDropdown from '@/components/operate-dropdown';
import { useDeleteFlow } from '@/hooks/flow-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { IFlow } from '@/interfaces/database/flow';
import classNames from 'classnames';
import { useCallback } from 'react';
import GraphAvatar from '../graph-avatar';
import styles from './index.less';
interface IProps {
item: IFlow;
onDelete?: (string: string) => void;
}
const FlowCard = ({ item }: IProps) => {
const navigate = useNavigate();
const { deleteFlow } = useDeleteFlow();
const { data: userInfo } = useFetchUserInfo();
const removeFlow = useCallback(() => {
return deleteFlow([item.id]);
@ -27,6 +31,13 @@ const FlowCard = ({ item }: IProps) => {
};
return (
<Badge.Ribbon
text={item?.nickname}
color={userInfo?.nickname === item?.nickname ? '#1677ff' : 'pink'}
className={classNames(styles.ribbon, {
[styles.hideRibbon]: item.permission !== 'team',
})}
>
<Card className={styles.card} onClick={handleCardClick}>
<div className={styles.container}>
<div className={styles.content}>
@ -54,6 +65,7 @@ const FlowCard = ({ item }: IProps) => {
</div>
</div>
</Card>
</Badge.Ribbon>
);
};

View File

@ -1,16 +1,56 @@
import { useSetModalState } from '@/hooks/common-hooks';
import {
useFetchFlowList,
useFetchFlowTemplates,
useSetFlow,
} from '@/hooks/flow-hooks';
import { useFetchFlowTemplates, useSetFlow } from '@/hooks/flow-hooks';
import { useHandleSearchChange } from '@/hooks/logic-hooks';
import flowService from '@/services/flow-service';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { useCallback } from 'react';
import { useNavigate } from 'umi';
export const useFetchDataOnMount = () => {
const { data, loading } = useFetchFlowList();
const { searchString, handleInputChange } = useHandleSearchChange();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
return { list: data, loading };
const PageSize = 30;
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['infiniteFetchFlowListTeam', debouncedSearchString],
queryFn: async ({ pageParam }) => {
const { data } = await flowService.listCanvasTeam({
page: pageParam,
page_size: PageSize,
keywords: debouncedSearchString,
});
const list = data?.data ?? [];
return list;
},
initialPageParam: 1,
getNextPageParam: (lastPage, pages, lastPageParam) => {
if (lastPageParam * PageSize <= lastPage.total) {
return lastPageParam + 1;
}
return undefined;
},
});
return {
data,
loading: isFetching,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
handleInputChange,
searchString,
};
};
export const useSaveFlow = () => {

View File

@ -1,10 +1,21 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Empty, Flex, Spin } from 'antd';
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
import {
Button,
Divider,
Empty,
Flex,
Input,
Skeleton,
Space,
Spin,
} from 'antd';
import AgentTemplateModal from './agent-template-modal';
import FlowCard from './flow-card';
import { useFetchDataOnMount, useSaveFlow } from './hooks';
import { useTranslate } from '@/hooks/common-hooks';
import { useMemo } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
import styles from './index.less';
const FlowList = () => {
@ -17,11 +28,37 @@ const FlowList = () => {
} = useSaveFlow();
const { t } = useTranslate('flow');
const { list, loading } = useFetchDataOnMount();
const {
data,
loading,
searchString,
handleInputChange,
fetchNextPage,
hasNextPage,
} = useFetchDataOnMount();
const nextList = useMemo(() => {
const list =
data?.pages?.flatMap((x) => (Array.isArray(x.kbs) ? x.kbs : [])) ?? [];
return list;
}, [data?.pages]);
const total = useMemo(() => {
return data?.pages.at(-1).total ?? 0;
}, [data?.pages]);
return (
<Flex className={styles.flowListWrapper} vertical flex={1} gap={'large'}>
<Flex justify={'end'}>
<Space size={'large'}>
<Input
placeholder={t('searchAgentPlaceholder')}
value={searchString}
style={{ width: 220 }}
allowClear
onChange={handleInputChange}
prefix={<SearchOutlined />}
/>
<Button
type="primary"
icon={<PlusOutlined />}
@ -29,17 +66,28 @@ const FlowList = () => {
>
{t('createGraph')}
</Button>
</Space>
</Flex>
<Spin spinning={loading}>
<InfiniteScroll
dataLength={nextList?.length ?? 0}
next={fetchNextPage}
hasMore={hasNextPage}
loader={<Skeleton avatar paragraph={{ rows: 1 }} active />}
endMessage={!!total && <Divider plain>{t('noMoreData')} 🤐</Divider>}
scrollableTarget="scrollableDiv"
>
<Flex gap={'large'} wrap="wrap" className={styles.flowCardContainer}>
{list.length > 0 ? (
list.map((item) => {
{nextList.length > 0 ? (
nextList.map((item) => {
return <FlowCard item={item} key={item.id}></FlowCard>;
})
) : (
<Empty className={styles.knowledgeEmpty}></Empty>
)}
</Flex>
</InfiniteScroll>
</Spin>
{flowSettingVisible && (
<AgentTemplateModal

View File

@ -16,6 +16,8 @@ const {
testDbConnect,
getInputElements,
debug,
listCanvasTeam,
settingCanvas,
} = api;
const methods = {
@ -71,6 +73,14 @@ const methods = {
url: debug,
method: 'post',
},
listCanvasTeam: {
url: listCanvasTeam,
method: 'get',
},
settingCanvas: {
url: settingCanvas,
method: 'post',
},
} as const;
const flowService = registerServer<keyof typeof methods>(methods, request);

View File

@ -123,10 +123,12 @@ export default {
// flow
listTemplates: `${api_host}/canvas/templates`,
listCanvas: `${api_host}/canvas/list`,
listCanvasTeam: `${api_host}/canvas/listteam`,
getCanvas: `${api_host}/canvas/get`,
getCanvasSSE: `${api_host}/canvas/getsse`,
removeCanvas: `${api_host}/canvas/rm`,
setCanvas: `${api_host}/canvas/set`,
settingCanvas: `${api_host}/canvas/setting`,
getListVersion: `${api_host}/canvas/getlistversion`,
getVersion: `${api_host}/canvas/getversion`,
resetCanvas: `${api_host}/canvas/reset`,