Merge branch 'main' into deploy/dev
# Conflicts: # api/core/rag/datasource/vdb/pgvector/pgvector.py # api/services/tag_service.py
This commit is contained in:
commit
967085d196
1
.github/workflows/build-push.yml
vendored
1
.github/workflows/build-push.yml
vendored
@ -5,6 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- "main"
|
- "main"
|
||||||
- "deploy/dev"
|
- "deploy/dev"
|
||||||
|
- "deploy/enterprise"
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
|
|||||||
29
.github/workflows/deploy-enterprise.yml
vendored
Normal file
29
.github/workflows/deploy-enterprise.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: Deploy Enterprise
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Build and Push API & Web"]
|
||||||
|
branches:
|
||||||
|
- "deploy/enterprise"
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.head_branch == 'deploy/enterprise'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@v0.1.8
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.ENTERPRISE_SSH_HOST }}
|
||||||
|
username: ${{ secrets.ENTERPRISE_SSH_USER }}
|
||||||
|
password: ${{ secrets.ENTERPRISE_SSH_PASSWORD }}
|
||||||
|
script: |
|
||||||
|
${{ vars.ENTERPRISE_SSH_SCRIPT || secrets.ENTERPRISE_SSH_SCRIPT }}
|
||||||
@ -378,6 +378,7 @@ HTTP_REQUEST_MAX_READ_TIMEOUT=600
|
|||||||
HTTP_REQUEST_MAX_WRITE_TIMEOUT=600
|
HTTP_REQUEST_MAX_WRITE_TIMEOUT=600
|
||||||
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
|
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
|
||||||
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
|
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY=True
|
||||||
|
|
||||||
# Respect X-* headers to redirect clients
|
# Respect X-* headers to redirect clients
|
||||||
RESPECT_XFORWARD_HEADERS_ENABLED=false
|
RESPECT_XFORWARD_HEADERS_ENABLED=false
|
||||||
|
|||||||
@ -332,6 +332,11 @@ class HttpConfig(BaseSettings):
|
|||||||
default=1 * 1024 * 1024,
|
default=1 * 1024 * 1024,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY: bool = Field(
|
||||||
|
description="Enable or disable SSL verification for HTTP requests",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
SSRF_DEFAULT_MAX_RETRIES: PositiveInt = Field(
|
SSRF_DEFAULT_MAX_RETRIES: PositiveInt = Field(
|
||||||
description="Maximum number of retries for network requests (SSRF)",
|
description="Maximum number of retries for network requests (SSRF)",
|
||||||
default=3,
|
default=3,
|
||||||
|
|||||||
@ -43,3 +43,8 @@ class PGVectorConfig(BaseSettings):
|
|||||||
description="Max connection of the PostgreSQL database",
|
description="Max connection of the PostgreSQL database",
|
||||||
default=5,
|
default=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PGVECTOR_PG_BIGM: bool = Field(
|
||||||
|
description="Whether to use pg_bigm module for full text search",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|||||||
@ -316,7 +316,7 @@ class AppTraceApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self, app_id):
|
def post(self, app_id):
|
||||||
# add app trace
|
# add app trace
|
||||||
if not current_user.is_editing_role:
|
if not current_user.is_editor:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("enabled", type=bool, required=True, location="json")
|
parser.add_argument("enabled", type=bool, required=True, location="json")
|
||||||
|
|||||||
@ -103,7 +103,9 @@ class DatasetConfigManager:
|
|||||||
dataset_configs["retrieval_model"]
|
dataset_configs["retrieval_model"]
|
||||||
),
|
),
|
||||||
top_k=dataset_configs.get("top_k", 4),
|
top_k=dataset_configs.get("top_k", 4),
|
||||||
score_threshold=dataset_configs.get("score_threshold"),
|
score_threshold=dataset_configs.get("score_threshold")
|
||||||
|
if dataset_configs.get("score_threshold_enabled", False)
|
||||||
|
else None,
|
||||||
reranking_model=dataset_configs.get("reranking_model"),
|
reranking_model=dataset_configs.get("reranking_model"),
|
||||||
weights=dataset_configs.get("weights"),
|
weights=dataset_configs.get("weights"),
|
||||||
reranking_enabled=dataset_configs.get("reranking_enabled", True),
|
reranking_enabled=dataset_configs.get("reranking_enabled", True),
|
||||||
|
|||||||
@ -17,17 +17,15 @@ class FileUploadConfigManager:
|
|||||||
if file_upload_dict:
|
if file_upload_dict:
|
||||||
if file_upload_dict.get("enabled"):
|
if file_upload_dict.get("enabled"):
|
||||||
transform_methods = file_upload_dict.get("allowed_file_upload_methods", [])
|
transform_methods = file_upload_dict.get("allowed_file_upload_methods", [])
|
||||||
data = {
|
file_upload_dict["image_config"] = {
|
||||||
"image_config": {
|
"number_limits": file_upload_dict.get("number_limits", 1),
|
||||||
"number_limits": file_upload_dict["number_limits"],
|
"transfer_methods": transform_methods,
|
||||||
"transfer_methods": transform_methods,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_vision:
|
if is_vision:
|
||||||
data["image_config"]["detail"] = file_upload_dict.get("image", {}).get("detail", "low")
|
file_upload_dict["image_config"]["detail"] = file_upload_dict.get("image", {}).get("detail", "high")
|
||||||
|
|
||||||
return FileUploadConfig.model_validate(data)
|
return FileUploadConfig.model_validate(file_upload_dict)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]:
|
def validate_and_set_defaults(cls, config: dict) -> tuple[dict, list[str]]:
|
||||||
|
|||||||
@ -11,6 +11,19 @@ from configs import dify_config
|
|||||||
|
|
||||||
SSRF_DEFAULT_MAX_RETRIES = dify_config.SSRF_DEFAULT_MAX_RETRIES
|
SSRF_DEFAULT_MAX_RETRIES = dify_config.SSRF_DEFAULT_MAX_RETRIES
|
||||||
|
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY = True # Default value for HTTP_REQUEST_NODE_SSL_VERIFY is True
|
||||||
|
try:
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY = dify_config.HTTP_REQUEST_NODE_SSL_VERIFY
|
||||||
|
http_request_node_ssl_verify_lower = str(HTTP_REQUEST_NODE_SSL_VERIFY).lower()
|
||||||
|
if http_request_node_ssl_verify_lower == "true":
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY = True
|
||||||
|
elif http_request_node_ssl_verify_lower == "false":
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY = False
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid value. HTTP_REQUEST_NODE_SSL_VERIFY should be 'True' or 'False'")
|
||||||
|
except NameError:
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY = True
|
||||||
|
|
||||||
BACKOFF_FACTOR = 0.5
|
BACKOFF_FACTOR = 0.5
|
||||||
STATUS_FORCELIST = [429, 500, 502, 503, 504]
|
STATUS_FORCELIST = [429, 500, 502, 503, 504]
|
||||||
|
|
||||||
@ -39,17 +52,17 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
|
|||||||
while retries <= max_retries:
|
while retries <= max_retries:
|
||||||
try:
|
try:
|
||||||
if dify_config.SSRF_PROXY_ALL_URL:
|
if dify_config.SSRF_PROXY_ALL_URL:
|
||||||
with httpx.Client(proxy=dify_config.SSRF_PROXY_ALL_URL) as client:
|
with httpx.Client(proxy=dify_config.SSRF_PROXY_ALL_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
|
||||||
response = client.request(method=method, url=url, **kwargs)
|
response = client.request(method=method, url=url, **kwargs)
|
||||||
elif dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL:
|
elif dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL:
|
||||||
proxy_mounts = {
|
proxy_mounts = {
|
||||||
"http://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTP_URL),
|
"http://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTP_URL),
|
||||||
"https://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTPS_URL),
|
"https://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTPS_URL),
|
||||||
}
|
}
|
||||||
with httpx.Client(mounts=proxy_mounts) as client:
|
with httpx.Client(mounts=proxy_mounts, verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
|
||||||
response = client.request(method=method, url=url, **kwargs)
|
response = client.request(method=method, url=url, **kwargs)
|
||||||
else:
|
else:
|
||||||
with httpx.Client() as client:
|
with httpx.Client(verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
|
||||||
response = client.request(method=method, url=url, **kwargs)
|
response = client.request(method=method, url=url, **kwargs)
|
||||||
|
|
||||||
if response.status_code not in STATUS_FORCELIST:
|
if response.status_code not in STATUS_FORCELIST:
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import json
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -258,7 +257,7 @@ class RetrievalService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def escape_query_for_search(query: str) -> str:
|
def escape_query_for_search(query: str) -> str:
|
||||||
return json.dumps(query).strip('"')
|
return query.replace('"', '\\"')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def format_retrieval_documents(cls, documents: list[Document]) -> list[RetrievalSegments]:
|
def format_retrieval_documents(cls, documents: list[Document]) -> list[RetrievalSegments]:
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class PGVectorConfig(BaseModel):
|
|||||||
database: str
|
database: str
|
||||||
min_connection: int
|
min_connection: int
|
||||||
max_connection: int
|
max_connection: int
|
||||||
|
pg_bigm: bool = False
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -62,12 +63,18 @@ CREATE INDEX IF NOT EXISTS embedding_cosine_v1_idx ON {table_name}
|
|||||||
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
|
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
SQL_CREATE_INDEX_PG_BIGM = """
|
||||||
|
CREATE INDEX IF NOT EXISTS bigm_idx ON {table_name}
|
||||||
|
USING gin (text gin_bigm_ops);
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PGVector(BaseVector):
|
class PGVector(BaseVector):
|
||||||
def __init__(self, collection_name: str, config: PGVectorConfig):
|
def __init__(self, collection_name: str, config: PGVectorConfig):
|
||||||
super().__init__(collection_name)
|
super().__init__(collection_name)
|
||||||
self.pool = self._create_connection_pool(config)
|
self.pool = self._create_connection_pool(config)
|
||||||
self.table_name = f"embedding_{collection_name}"
|
self.table_name = f"embedding_{collection_name}"
|
||||||
|
self.pg_bigm = config.pg_bigm
|
||||||
|
|
||||||
def get_type(self) -> str:
|
def get_type(self) -> str:
|
||||||
return VectorType.PGVECTOR
|
return VectorType.PGVECTOR
|
||||||
@ -187,16 +194,29 @@ class PGVector(BaseVector):
|
|||||||
if document_ids_filter:
|
if document_ids_filter:
|
||||||
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
|
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
|
||||||
where_clause = f" AND metadata->>'document_id' in ({document_ids}) "
|
where_clause = f" AND metadata->>'document_id' in ({document_ids}) "
|
||||||
cur.execute(
|
if self.pg_bigm:
|
||||||
f"""SELECT meta, text, ts_rank(to_tsvector(coalesce(text, '')), plainto_tsquery(%s)) AS score
|
cur.execute("SET pg_bigm.similarity_limit TO 0.000001")
|
||||||
FROM {self.table_name}
|
cur.execute(
|
||||||
WHERE to_tsvector(text) @@ plainto_tsquery(%s)
|
f"""SELECT meta, text, bigm_similarity(unistr(%s), coalesce(text, '')) AS score
|
||||||
{where_clause}
|
FROM {self.table_name}
|
||||||
ORDER BY score DESC
|
WHERE text =%% unistr(%s)
|
||||||
LIMIT {top_k}""",
|
{where_clause}
|
||||||
# f"'{query}'" is required in order to account for whitespace in query
|
ORDER BY score DESC
|
||||||
(f"'{query}'", f"'{query}'"),
|
LIMIT {top_k}""",
|
||||||
)
|
# f"'{query}'" is required in order to account for whitespace in query
|
||||||
|
(f"'{query}'", f"'{query}'"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute(
|
||||||
|
f"""SELECT meta, text, ts_rank(to_tsvector(coalesce(text, '')), plainto_tsquery(%s)) AS score
|
||||||
|
FROM {self.table_name}
|
||||||
|
WHERE to_tsvector(text) @@ plainto_tsquery(%s)
|
||||||
|
{where_clause}
|
||||||
|
ORDER BY score DESC
|
||||||
|
LIMIT {top_k}""",
|
||||||
|
# f"'{query}'" is required in order to account for whitespace in query
|
||||||
|
(f"'{query}'", f"'{query}'"),
|
||||||
|
)
|
||||||
|
|
||||||
docs = []
|
docs = []
|
||||||
|
|
||||||
@ -226,6 +246,9 @@ class PGVector(BaseVector):
|
|||||||
# ref: https://github.com/pgvector/pgvector?tab=readme-ov-file#indexing
|
# ref: https://github.com/pgvector/pgvector?tab=readme-ov-file#indexing
|
||||||
if dimension <= 2000:
|
if dimension <= 2000:
|
||||||
cur.execute(SQL_CREATE_INDEX.format(table_name=self.table_name))
|
cur.execute(SQL_CREATE_INDEX.format(table_name=self.table_name))
|
||||||
|
if self.pg_bigm:
|
||||||
|
cur.execute("CREATE EXTENSION IF NOT EXISTS pg_bigm")
|
||||||
|
cur.execute(SQL_CREATE_INDEX_PG_BIGM.format(table_name=self.table_name))
|
||||||
redis_client.set(collection_exist_cache_key, 1, ex=3600)
|
redis_client.set(collection_exist_cache_key, 1, ex=3600)
|
||||||
|
|
||||||
|
|
||||||
@ -249,5 +272,6 @@ class PGVectorFactory(AbstractVectorFactory):
|
|||||||
database=dify_config.PGVECTOR_DATABASE or "postgres",
|
database=dify_config.PGVECTOR_DATABASE or "postgres",
|
||||||
min_connection=dify_config.PGVECTOR_MIN_CONNECTION,
|
min_connection=dify_config.PGVECTOR_MIN_CONNECTION,
|
||||||
max_connection=dify_config.PGVECTOR_MAX_CONNECTION,
|
max_connection=dify_config.PGVECTOR_MAX_CONNECTION,
|
||||||
|
pg_bigm=dify_config.PGVECTOR_PG_BIGM,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -76,16 +76,20 @@ class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter)
|
|||||||
|
|
||||||
def recursive_split_text(self, text: str) -> list[str]:
|
def recursive_split_text(self, text: str) -> list[str]:
|
||||||
"""Split incoming text and return chunks."""
|
"""Split incoming text and return chunks."""
|
||||||
|
|
||||||
final_chunks = []
|
final_chunks = []
|
||||||
# Get appropriate separator to use
|
|
||||||
separator = self._separators[-1]
|
separator = self._separators[-1]
|
||||||
for _s in self._separators:
|
new_separators = []
|
||||||
|
|
||||||
|
for i, _s in enumerate(self._separators):
|
||||||
if _s == "":
|
if _s == "":
|
||||||
separator = _s
|
separator = _s
|
||||||
break
|
break
|
||||||
if _s in text:
|
if _s in text:
|
||||||
separator = _s
|
separator = _s
|
||||||
|
new_separators = self._separators[i + 1 :]
|
||||||
break
|
break
|
||||||
|
|
||||||
# Now that we have the separator, split the text
|
# Now that we have the separator, split the text
|
||||||
if separator:
|
if separator:
|
||||||
if separator == " ":
|
if separator == " ":
|
||||||
@ -94,23 +98,52 @@ class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter)
|
|||||||
splits = text.split(separator)
|
splits = text.split(separator)
|
||||||
else:
|
else:
|
||||||
splits = list(text)
|
splits = list(text)
|
||||||
# Now go merging things, recursively splitting longer texts.
|
splits = [s for s in splits if (s not in {"", "\n"})]
|
||||||
_good_splits = []
|
_good_splits = []
|
||||||
_good_splits_lengths = [] # cache the lengths of the splits
|
_good_splits_lengths = [] # cache the lengths of the splits
|
||||||
|
_separator = "" if self._keep_separator else separator
|
||||||
s_lens = self._length_function(splits)
|
s_lens = self._length_function(splits)
|
||||||
for s, s_len in zip(splits, s_lens):
|
if _separator != "":
|
||||||
if s_len < self._chunk_size:
|
for s, s_len in zip(splits, s_lens):
|
||||||
_good_splits.append(s)
|
if s_len < self._chunk_size:
|
||||||
_good_splits_lengths.append(s_len)
|
_good_splits.append(s)
|
||||||
else:
|
_good_splits_lengths.append(s_len)
|
||||||
if _good_splits:
|
else:
|
||||||
merged_text = self._merge_splits(_good_splits, separator, _good_splits_lengths)
|
if _good_splits:
|
||||||
final_chunks.extend(merged_text)
|
merged_text = self._merge_splits(_good_splits, _separator, _good_splits_lengths)
|
||||||
_good_splits = []
|
final_chunks.extend(merged_text)
|
||||||
_good_splits_lengths = []
|
_good_splits = []
|
||||||
other_info = self.recursive_split_text(s)
|
_good_splits_lengths = []
|
||||||
final_chunks.extend(other_info)
|
if not new_separators:
|
||||||
if _good_splits:
|
final_chunks.append(s)
|
||||||
merged_text = self._merge_splits(_good_splits, separator, _good_splits_lengths)
|
else:
|
||||||
final_chunks.extend(merged_text)
|
other_info = self._split_text(s, new_separators)
|
||||||
|
final_chunks.extend(other_info)
|
||||||
|
|
||||||
|
if _good_splits:
|
||||||
|
merged_text = self._merge_splits(_good_splits, _separator, _good_splits_lengths)
|
||||||
|
final_chunks.extend(merged_text)
|
||||||
|
else:
|
||||||
|
current_part = ""
|
||||||
|
current_length = 0
|
||||||
|
overlap_part = ""
|
||||||
|
overlap_part_length = 0
|
||||||
|
for s, s_len in zip(splits, s_lens):
|
||||||
|
if current_length + s_len <= self._chunk_size - self._chunk_overlap:
|
||||||
|
current_part += s
|
||||||
|
current_length += s_len
|
||||||
|
elif current_length + s_len <= self._chunk_size:
|
||||||
|
current_part += s
|
||||||
|
current_length += s_len
|
||||||
|
overlap_part += s
|
||||||
|
overlap_part_length += s_len
|
||||||
|
else:
|
||||||
|
final_chunks.append(current_part)
|
||||||
|
current_part = overlap_part + s
|
||||||
|
current_length = s_len + overlap_part_length
|
||||||
|
overlap_part = ""
|
||||||
|
overlap_part_length = 0
|
||||||
|
if current_part:
|
||||||
|
final_chunks.append(current_part)
|
||||||
|
|
||||||
return final_chunks
|
return final_chunks
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from core.file import File, FileAttribute, file_manager
|
from core.file import File, FileAttribute, file_manager
|
||||||
from core.variables import Segment, SegmentGroup, Variable
|
from core.variables import Segment, SegmentGroup, Variable
|
||||||
from core.variables.segments import FileSegment
|
from core.variables.segments import FileSegment, NoneSegment
|
||||||
from factories import variable_factory
|
from factories import variable_factory
|
||||||
|
|
||||||
from ..constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
|
from ..constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
|
||||||
@ -131,11 +131,13 @@ class VariablePool(BaseModel):
|
|||||||
if attr not in {item.value for item in FileAttribute}:
|
if attr not in {item.value for item in FileAttribute}:
|
||||||
return None
|
return None
|
||||||
value = self.get(selector)
|
value = self.get(selector)
|
||||||
if not isinstance(value, FileSegment):
|
if not isinstance(value, (FileSegment, NoneSegment)):
|
||||||
return None
|
return None
|
||||||
attr = FileAttribute(attr)
|
if isinstance(value, FileSegment):
|
||||||
attr_value = file_manager.get_attr(file=value.value, attr=attr)
|
attr = FileAttribute(attr)
|
||||||
return variable_factory.build_segment(attr_value)
|
attr_value = file_manager.get_attr(file=value.value, attr=attr)
|
||||||
|
return variable_factory.build_segment(attr_value)
|
||||||
|
return value
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import httpx
|
|||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from core.file import file_manager
|
from core.file import file_manager
|
||||||
from core.helper import ssrf_proxy
|
from core.helper import ssrf_proxy
|
||||||
|
from core.variables.segments import ArrayFileSegment, FileSegment
|
||||||
from core.workflow.entities.variable_pool import VariablePool
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
|
|
||||||
from .entities import (
|
from .entities import (
|
||||||
@ -57,7 +58,7 @@ class Executor:
|
|||||||
params: list[tuple[str, str]] | None
|
params: list[tuple[str, str]] | None
|
||||||
content: str | bytes | None
|
content: str | bytes | None
|
||||||
data: Mapping[str, Any] | None
|
data: Mapping[str, Any] | None
|
||||||
files: Mapping[str, tuple[str | None, bytes, str]] | None
|
files: list[tuple[str, tuple[str | None, bytes, str]]] | None
|
||||||
json: Any
|
json: Any
|
||||||
headers: dict[str, str]
|
headers: dict[str, str]
|
||||||
auth: HttpRequestNodeAuthorization
|
auth: HttpRequestNodeAuthorization
|
||||||
@ -207,17 +208,38 @@ class Executor:
|
|||||||
self.variable_pool.convert_template(item.key).text: item.file
|
self.variable_pool.convert_template(item.key).text: item.file
|
||||||
for item in filter(lambda item: item.type == "file", data)
|
for item in filter(lambda item: item.type == "file", data)
|
||||||
}
|
}
|
||||||
files: dict[str, Any] = {}
|
|
||||||
files = {k: self.variable_pool.get_file(selector) for k, selector in file_selectors.items()}
|
# get files from file_selectors, add support for array file variables
|
||||||
files = {k: v for k, v in files.items() if v is not None}
|
files_list = []
|
||||||
files = {k: variable.value for k, variable in files.items() if variable is not None}
|
for key, selector in file_selectors.items():
|
||||||
files = {
|
segment = self.variable_pool.get(selector)
|
||||||
k: (v.filename, file_manager.download(v), v.mime_type or "application/octet-stream")
|
if isinstance(segment, FileSegment):
|
||||||
for k, v in files.items()
|
files_list.append((key, [segment.value]))
|
||||||
if v.related_id is not None
|
elif isinstance(segment, ArrayFileSegment):
|
||||||
}
|
files_list.append((key, list(segment.value)))
|
||||||
|
|
||||||
|
# get files from file_manager
|
||||||
|
files: dict[str, list[tuple[str | None, bytes, str]]] = {}
|
||||||
|
for key, files_in_segment in files_list:
|
||||||
|
for file in files_in_segment:
|
||||||
|
if file.related_id is not None:
|
||||||
|
file_tuple = (
|
||||||
|
file.filename,
|
||||||
|
file_manager.download(file),
|
||||||
|
file.mime_type or "application/octet-stream",
|
||||||
|
)
|
||||||
|
if key not in files:
|
||||||
|
files[key] = []
|
||||||
|
files[key].append(file_tuple)
|
||||||
|
|
||||||
|
# convert files to list for httpx request
|
||||||
|
if files:
|
||||||
|
self.files = []
|
||||||
|
for key, file_tuples in files.items():
|
||||||
|
for file_tuple in file_tuples:
|
||||||
|
self.files.append((key, file_tuple))
|
||||||
|
|
||||||
self.data = form_data
|
self.data = form_data
|
||||||
self.files = files or None
|
|
||||||
|
|
||||||
def _assembling_headers(self) -> dict[str, Any]:
|
def _assembling_headers(self) -> dict[str, Any]:
|
||||||
authorization = deepcopy(self.auth)
|
authorization = deepcopy(self.auth)
|
||||||
@ -344,10 +366,16 @@ class Executor:
|
|||||||
|
|
||||||
body_string = ""
|
body_string = ""
|
||||||
if self.files:
|
if self.files:
|
||||||
for k, v in self.files.items():
|
for key, (filename, content, mime_type) in self.files:
|
||||||
body_string += f"--{boundary}\r\n"
|
body_string += f"--{boundary}\r\n"
|
||||||
body_string += f'Content-Disposition: form-data; name="{k}"\r\n\r\n'
|
body_string += f'Content-Disposition: form-data; name="{key}"\r\n\r\n'
|
||||||
body_string += f"{v[1]}\r\n"
|
# decode content
|
||||||
|
try:
|
||||||
|
body_string += content.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# fix: decode binary content
|
||||||
|
pass
|
||||||
|
body_string += "\r\n"
|
||||||
body_string += f"--{boundary}--\r\n"
|
body_string += f"--{boundary}--\r\n"
|
||||||
elif self.node_data.body:
|
elif self.node_data.body:
|
||||||
if self.content:
|
if self.content:
|
||||||
|
|||||||
@ -102,6 +102,8 @@ class ApiToolProvider(Base):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self) -> Account | None:
|
def user(self) -> Account | None:
|
||||||
|
if not self.user_id:
|
||||||
|
return None
|
||||||
return db.session.query(Account).filter(Account.id == self.user_id).first()
|
return db.session.query(Account).filter(Account.id == self.user_id).first()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@ -88,6 +88,7 @@ class RetrievalModel(BaseModel):
|
|||||||
search_method: Literal["hybrid_search", "semantic_search", "full_text_search"]
|
search_method: Literal["hybrid_search", "semantic_search", "full_text_search"]
|
||||||
reranking_enable: bool
|
reranking_enable: bool
|
||||||
reranking_model: Optional[RerankingModel] = None
|
reranking_model: Optional[RerankingModel] = None
|
||||||
|
reranking_mode: Optional[str] = None
|
||||||
top_k: int
|
top_k: int
|
||||||
score_threshold_enabled: bool
|
score_threshold_enabled: bool
|
||||||
score_threshold: Optional[float] = None
|
score_threshold: Optional[float] = None
|
||||||
|
|||||||
@ -66,7 +66,7 @@ class SystemFeatureModel(BaseModel):
|
|||||||
sso_enforced_for_web: bool = False
|
sso_enforced_for_web: bool = False
|
||||||
sso_enforced_for_web_protocol: str = ""
|
sso_enforced_for_web_protocol: str = ""
|
||||||
enable_web_sso_switch_component: bool = False
|
enable_web_sso_switch_component: bool = False
|
||||||
enable_marketplace: bool = True
|
enable_marketplace: bool = False
|
||||||
max_plugin_package_size: int = dify_config.PLUGIN_MAX_PACKAGE_SIZE
|
max_plugin_package_size: int = dify_config.PLUGIN_MAX_PACKAGE_SIZE
|
||||||
enable_email_code_login: bool = False
|
enable_email_code_login: bool = False
|
||||||
enable_email_password_login: bool = True
|
enable_email_password_login: bool = True
|
||||||
|
|||||||
@ -18,7 +18,9 @@ def test_convert_with_vision():
|
|||||||
number_limits=5,
|
number_limits=5,
|
||||||
transfer_methods=[FileTransferMethod.REMOTE_URL],
|
transfer_methods=[FileTransferMethod.REMOTE_URL],
|
||||||
detail=ImagePromptMessageContent.DETAIL.HIGH,
|
detail=ImagePromptMessageContent.DETAIL.HIGH,
|
||||||
)
|
),
|
||||||
|
allowed_file_upload_methods=[FileTransferMethod.REMOTE_URL],
|
||||||
|
number_limits=5,
|
||||||
)
|
)
|
||||||
assert result == expected
|
assert result == expected
|
||||||
|
|
||||||
@ -33,7 +35,9 @@ def test_convert_without_vision():
|
|||||||
}
|
}
|
||||||
result = FileUploadConfigManager.convert(config, is_vision=False)
|
result = FileUploadConfigManager.convert(config, is_vision=False)
|
||||||
expected = FileUploadConfig(
|
expected = FileUploadConfig(
|
||||||
image_config=ImageConfig(number_limits=5, transfer_methods=[FileTransferMethod.REMOTE_URL])
|
image_config=ImageConfig(number_limits=5, transfer_methods=[FileTransferMethod.REMOTE_URL]),
|
||||||
|
allowed_file_upload_methods=[FileTransferMethod.REMOTE_URL],
|
||||||
|
number_limits=5,
|
||||||
)
|
)
|
||||||
assert result == expected
|
assert result == expected
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import httpx
|
|||||||
|
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.file import File, FileTransferMethod, FileType
|
from core.file import File, FileTransferMethod, FileType
|
||||||
from core.variables import FileVariable
|
from core.variables import ArrayFileVariable, FileVariable
|
||||||
from core.workflow.entities.variable_pool import VariablePool
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState
|
from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState
|
||||||
from core.workflow.nodes.answer import AnswerStreamGenerateRoute
|
from core.workflow.nodes.answer import AnswerStreamGenerateRoute
|
||||||
@ -183,7 +183,7 @@ def test_http_request_node_form_with_file(monkeypatch):
|
|||||||
|
|
||||||
def attr_checker(*args, **kwargs):
|
def attr_checker(*args, **kwargs):
|
||||||
assert kwargs["data"] == {"name": "test"}
|
assert kwargs["data"] == {"name": "test"}
|
||||||
assert kwargs["files"] == {"file": (None, b"test", "application/octet-stream")}
|
assert kwargs["files"] == [("file", (None, b"test", "application/octet-stream"))]
|
||||||
return httpx.Response(200, content=b"")
|
return httpx.Response(200, content=b"")
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
@ -194,3 +194,131 @@ def test_http_request_node_form_with_file(monkeypatch):
|
|||||||
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
|
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
|
||||||
assert result.outputs is not None
|
assert result.outputs is not None
|
||||||
assert result.outputs["body"] == ""
|
assert result.outputs["body"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_http_request_node_form_with_multiple_files(monkeypatch):
|
||||||
|
data = HttpRequestNodeData(
|
||||||
|
title="test",
|
||||||
|
method="post",
|
||||||
|
url="http://example.org/upload",
|
||||||
|
authorization=HttpRequestNodeAuthorization(type="no-auth"),
|
||||||
|
headers="",
|
||||||
|
params="",
|
||||||
|
body=HttpRequestNodeBody(
|
||||||
|
type="form-data",
|
||||||
|
data=[
|
||||||
|
BodyData(
|
||||||
|
key="files",
|
||||||
|
type="file",
|
||||||
|
file=["1111", "files"],
|
||||||
|
),
|
||||||
|
BodyData(
|
||||||
|
key="name",
|
||||||
|
type="text",
|
||||||
|
value="test",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
variable_pool = VariablePool(
|
||||||
|
system_variables={},
|
||||||
|
user_inputs={},
|
||||||
|
)
|
||||||
|
|
||||||
|
files = [
|
||||||
|
File(
|
||||||
|
tenant_id="1",
|
||||||
|
type=FileType.IMAGE,
|
||||||
|
transfer_method=FileTransferMethod.LOCAL_FILE,
|
||||||
|
related_id="file1",
|
||||||
|
filename="image1.jpg",
|
||||||
|
mime_type="image/jpeg",
|
||||||
|
storage_key="",
|
||||||
|
),
|
||||||
|
File(
|
||||||
|
tenant_id="1",
|
||||||
|
type=FileType.DOCUMENT,
|
||||||
|
transfer_method=FileTransferMethod.LOCAL_FILE,
|
||||||
|
related_id="file2",
|
||||||
|
filename="document.pdf",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
storage_key="",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
variable_pool.add(
|
||||||
|
["1111", "files"],
|
||||||
|
ArrayFileVariable(
|
||||||
|
name="files",
|
||||||
|
value=files,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
node = HttpRequestNode(
|
||||||
|
id="1",
|
||||||
|
config={
|
||||||
|
"id": "1",
|
||||||
|
"data": data.model_dump(),
|
||||||
|
},
|
||||||
|
graph_init_params=GraphInitParams(
|
||||||
|
tenant_id="1",
|
||||||
|
app_id="1",
|
||||||
|
workflow_type=WorkflowType.WORKFLOW,
|
||||||
|
workflow_id="1",
|
||||||
|
graph_config={},
|
||||||
|
user_id="1",
|
||||||
|
user_from=UserFrom.ACCOUNT,
|
||||||
|
invoke_from=InvokeFrom.SERVICE_API,
|
||||||
|
call_depth=0,
|
||||||
|
),
|
||||||
|
graph=Graph(
|
||||||
|
root_node_id="1",
|
||||||
|
answer_stream_generate_routes=AnswerStreamGenerateRoute(
|
||||||
|
answer_dependencies={},
|
||||||
|
answer_generate_route={},
|
||||||
|
),
|
||||||
|
end_stream_param=EndStreamParam(
|
||||||
|
end_dependencies={},
|
||||||
|
end_stream_variable_selector_mapping={},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
graph_runtime_state=GraphRuntimeState(
|
||||||
|
variable_pool=variable_pool,
|
||||||
|
start_at=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"core.workflow.nodes.http_request.executor.file_manager.download",
|
||||||
|
lambda file: b"test_image_data" if file.mime_type == "image/jpeg" else b"test_pdf_data",
|
||||||
|
)
|
||||||
|
|
||||||
|
def attr_checker(*args, **kwargs):
|
||||||
|
assert kwargs["data"] == {"name": "test"}
|
||||||
|
|
||||||
|
assert len(kwargs["files"]) == 2
|
||||||
|
assert kwargs["files"][0][0] == "files"
|
||||||
|
assert kwargs["files"][1][0] == "files"
|
||||||
|
|
||||||
|
file_tuples = [f[1] for f in kwargs["files"]]
|
||||||
|
file_contents = [f[1] for f in file_tuples]
|
||||||
|
file_types = [f[2] for f in file_tuples]
|
||||||
|
|
||||||
|
assert b"test_image_data" in file_contents
|
||||||
|
assert b"test_pdf_data" in file_contents
|
||||||
|
assert "image/jpeg" in file_types
|
||||||
|
assert "application/pdf" in file_types
|
||||||
|
|
||||||
|
return httpx.Response(200, content=b'{"status":"success"}')
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"core.helper.ssrf_proxy.post",
|
||||||
|
attr_checker,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = node._run()
|
||||||
|
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
|
||||||
|
assert result.outputs is not None
|
||||||
|
assert result.outputs["body"] == '{"status":"success"}'
|
||||||
|
print(result.outputs["body"])
|
||||||
|
|||||||
@ -397,12 +397,12 @@ QDRANT_CLIENT_TIMEOUT=20
|
|||||||
QDRANT_GRPC_ENABLED=false
|
QDRANT_GRPC_ENABLED=false
|
||||||
QDRANT_GRPC_PORT=6334
|
QDRANT_GRPC_PORT=6334
|
||||||
|
|
||||||
# Milvus configuration Only available when VECTOR_STORE is `milvus`.
|
# Milvus configuration. Only available when VECTOR_STORE is `milvus`.
|
||||||
# The milvus uri.
|
# The milvus uri.
|
||||||
MILVUS_URI=http://127.0.0.1:19530
|
MILVUS_URI=http://host.docker.internal:19530
|
||||||
MILVUS_TOKEN=
|
MILVUS_TOKEN=
|
||||||
MILVUS_USER=root
|
MILVUS_USER=
|
||||||
MILVUS_PASSWORD=Milvus
|
MILVUS_PASSWORD=
|
||||||
MILVUS_ENABLE_HYBRID_SEARCH=False
|
MILVUS_ENABLE_HYBRID_SEARCH=False
|
||||||
|
|
||||||
# MyScale configuration, only available when VECTOR_STORE is `myscale`
|
# MyScale configuration, only available when VECTOR_STORE is `myscale`
|
||||||
@ -431,6 +431,8 @@ PGVECTOR_PASSWORD=difyai123456
|
|||||||
PGVECTOR_DATABASE=dify
|
PGVECTOR_DATABASE=dify
|
||||||
PGVECTOR_MIN_CONNECTION=1
|
PGVECTOR_MIN_CONNECTION=1
|
||||||
PGVECTOR_MAX_CONNECTION=5
|
PGVECTOR_MAX_CONNECTION=5
|
||||||
|
PGVECTOR_PG_BIGM=false
|
||||||
|
PGVECTOR_PG_BIGM_VERSION=1.2-20240606
|
||||||
|
|
||||||
# pgvecto-rs configurations, only available when VECTOR_STORE is `pgvecto-rs`
|
# pgvecto-rs configurations, only available when VECTOR_STORE is `pgvecto-rs`
|
||||||
PGVECTO_RS_HOST=pgvecto-rs
|
PGVECTO_RS_HOST=pgvecto-rs
|
||||||
@ -714,6 +716,7 @@ WORKFLOW_FILE_UPLOAD_LIMIT=10
|
|||||||
# HTTP request node in workflow configuration
|
# HTTP request node in workflow configuration
|
||||||
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
|
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
|
||||||
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
|
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY=True
|
||||||
|
|
||||||
# SSRF Proxy server HTTP URL
|
# SSRF Proxy server HTTP URL
|
||||||
SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128
|
SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128
|
||||||
|
|||||||
@ -322,8 +322,13 @@ services:
|
|||||||
POSTGRES_DB: ${PGVECTOR_POSTGRES_DB:-dify}
|
POSTGRES_DB: ${PGVECTOR_POSTGRES_DB:-dify}
|
||||||
# postgres data directory
|
# postgres data directory
|
||||||
PGDATA: ${PGVECTOR_PGDATA:-/var/lib/postgresql/data/pgdata}
|
PGDATA: ${PGVECTOR_PGDATA:-/var/lib/postgresql/data/pgdata}
|
||||||
|
# pg_bigm module for full text search
|
||||||
|
PG_BIGM: ${PGVECTOR_PG_BIGM:-false}
|
||||||
|
PG_BIGM_VERSION: ${PGVECTOR_PG_BIGM_VERSION:-1.2-20240606}
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/pgvector/data:/var/lib/postgresql/data
|
- ./volumes/pgvector/data:/var/lib/postgresql/data
|
||||||
|
- ./pgvector/docker-entrypoint.sh:/docker-entrypoint.sh
|
||||||
|
entrypoint: [ '/docker-entrypoint.sh' ]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ 'CMD', 'pg_isready' ]
|
test: [ 'CMD', 'pg_isready' ]
|
||||||
interval: 1s
|
interval: 1s
|
||||||
|
|||||||
@ -134,10 +134,10 @@ x-shared-env: &shared-api-worker-env
|
|||||||
QDRANT_CLIENT_TIMEOUT: ${QDRANT_CLIENT_TIMEOUT:-20}
|
QDRANT_CLIENT_TIMEOUT: ${QDRANT_CLIENT_TIMEOUT:-20}
|
||||||
QDRANT_GRPC_ENABLED: ${QDRANT_GRPC_ENABLED:-false}
|
QDRANT_GRPC_ENABLED: ${QDRANT_GRPC_ENABLED:-false}
|
||||||
QDRANT_GRPC_PORT: ${QDRANT_GRPC_PORT:-6334}
|
QDRANT_GRPC_PORT: ${QDRANT_GRPC_PORT:-6334}
|
||||||
MILVUS_URI: ${MILVUS_URI:-http://127.0.0.1:19530}
|
MILVUS_URI: ${MILVUS_URI:-http://host.docker.internal:19530}
|
||||||
MILVUS_TOKEN: ${MILVUS_TOKEN:-}
|
MILVUS_TOKEN: ${MILVUS_TOKEN:-}
|
||||||
MILVUS_USER: ${MILVUS_USER:-root}
|
MILVUS_USER: ${MILVUS_USER:-}
|
||||||
MILVUS_PASSWORD: ${MILVUS_PASSWORD:-Milvus}
|
MILVUS_PASSWORD: ${MILVUS_PASSWORD:-}
|
||||||
MILVUS_ENABLE_HYBRID_SEARCH: ${MILVUS_ENABLE_HYBRID_SEARCH:-False}
|
MILVUS_ENABLE_HYBRID_SEARCH: ${MILVUS_ENABLE_HYBRID_SEARCH:-False}
|
||||||
MYSCALE_HOST: ${MYSCALE_HOST:-myscale}
|
MYSCALE_HOST: ${MYSCALE_HOST:-myscale}
|
||||||
MYSCALE_PORT: ${MYSCALE_PORT:-8123}
|
MYSCALE_PORT: ${MYSCALE_PORT:-8123}
|
||||||
@ -157,6 +157,8 @@ x-shared-env: &shared-api-worker-env
|
|||||||
PGVECTOR_DATABASE: ${PGVECTOR_DATABASE:-dify}
|
PGVECTOR_DATABASE: ${PGVECTOR_DATABASE:-dify}
|
||||||
PGVECTOR_MIN_CONNECTION: ${PGVECTOR_MIN_CONNECTION:-1}
|
PGVECTOR_MIN_CONNECTION: ${PGVECTOR_MIN_CONNECTION:-1}
|
||||||
PGVECTOR_MAX_CONNECTION: ${PGVECTOR_MAX_CONNECTION:-5}
|
PGVECTOR_MAX_CONNECTION: ${PGVECTOR_MAX_CONNECTION:-5}
|
||||||
|
PGVECTOR_PG_BIGM: ${PGVECTOR_PG_BIGM:-false}
|
||||||
|
PGVECTOR_PG_BIGM_VERSION: ${PGVECTOR_PG_BIGM_VERSION:-1.2-20240606}
|
||||||
PGVECTO_RS_HOST: ${PGVECTO_RS_HOST:-pgvecto-rs}
|
PGVECTO_RS_HOST: ${PGVECTO_RS_HOST:-pgvecto-rs}
|
||||||
PGVECTO_RS_PORT: ${PGVECTO_RS_PORT:-5432}
|
PGVECTO_RS_PORT: ${PGVECTO_RS_PORT:-5432}
|
||||||
PGVECTO_RS_USER: ${PGVECTO_RS_USER:-postgres}
|
PGVECTO_RS_USER: ${PGVECTO_RS_USER:-postgres}
|
||||||
@ -308,6 +310,7 @@ x-shared-env: &shared-api-worker-env
|
|||||||
WORKFLOW_FILE_UPLOAD_LIMIT: ${WORKFLOW_FILE_UPLOAD_LIMIT:-10}
|
WORKFLOW_FILE_UPLOAD_LIMIT: ${WORKFLOW_FILE_UPLOAD_LIMIT:-10}
|
||||||
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760}
|
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760}
|
||||||
HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576}
|
HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576}
|
||||||
|
HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True}
|
||||||
SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128}
|
SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128}
|
||||||
SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128}
|
SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128}
|
||||||
LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100}
|
LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100}
|
||||||
@ -741,8 +744,13 @@ services:
|
|||||||
POSTGRES_DB: ${PGVECTOR_POSTGRES_DB:-dify}
|
POSTGRES_DB: ${PGVECTOR_POSTGRES_DB:-dify}
|
||||||
# postgres data directory
|
# postgres data directory
|
||||||
PGDATA: ${PGVECTOR_PGDATA:-/var/lib/postgresql/data/pgdata}
|
PGDATA: ${PGVECTOR_PGDATA:-/var/lib/postgresql/data/pgdata}
|
||||||
|
# pg_bigm module for full text search
|
||||||
|
PG_BIGM: ${PGVECTOR_PG_BIGM:-false}
|
||||||
|
PG_BIGM_VERSION: ${PGVECTOR_PG_BIGM_VERSION:-1.2-20240606}
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/pgvector/data:/var/lib/postgresql/data
|
- ./volumes/pgvector/data:/var/lib/postgresql/data
|
||||||
|
- ./pgvector/docker-entrypoint.sh:/docker-entrypoint.sh
|
||||||
|
entrypoint: [ '/docker-entrypoint.sh' ]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ 'CMD', 'pg_isready' ]
|
test: [ 'CMD', 'pg_isready' ]
|
||||||
interval: 1s
|
interval: 1s
|
||||||
|
|||||||
24
docker/pgvector/docker-entrypoint.sh
Executable file
24
docker/pgvector/docker-entrypoint.sh
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PG_MAJOR=16
|
||||||
|
|
||||||
|
if [ "${PG_BIGM}" = "true" ]; then
|
||||||
|
# install pg_bigm
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y curl make gcc postgresql-server-dev-${PG_MAJOR}
|
||||||
|
|
||||||
|
curl -LO https://github.com/pgbigm/pg_bigm/archive/refs/tags/v${PG_BIGM_VERSION}.tar.gz
|
||||||
|
tar xf v${PG_BIGM_VERSION}.tar.gz
|
||||||
|
cd pg_bigm-${PG_BIGM_VERSION} || exit 1
|
||||||
|
make USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config
|
||||||
|
make USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config install
|
||||||
|
|
||||||
|
cd - || exit 1
|
||||||
|
rm -rf v${PG_BIGM_VERSION}.tar.gz pg_bigm-${PG_BIGM_VERSION}
|
||||||
|
|
||||||
|
# enable pg_bigm
|
||||||
|
sed -i -e 's/^#\s*shared_preload_libraries.*/shared_preload_libraries = '\''pg_bigm'\''/' /var/lib/postgresql/data/pgdata/postgresql.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the original entrypoint script
|
||||||
|
exec /usr/local/bin/docker-entrypoint.sh postgres
|
||||||
@ -82,7 +82,7 @@ const Panel: FC = () => {
|
|||||||
? LangfuseIcon
|
? LangfuseIcon
|
||||||
: inUseTracingProvider === TracingProvider.opik
|
: inUseTracingProvider === TracingProvider.opik
|
||||||
? OpikIcon
|
? OpikIcon
|
||||||
: null
|
: LangsmithIcon
|
||||||
|
|
||||||
const [langSmithConfig, setLangSmithConfig] = useState<LangSmithConfig | null>(null)
|
const [langSmithConfig, setLangSmithConfig] = useState<LangSmithConfig | null>(null)
|
||||||
const [langFuseConfig, setLangFuseConfig] = useState<LangFuseConfig | null>(null)
|
const [langFuseConfig, setLangFuseConfig] = useState<LangFuseConfig | null>(null)
|
||||||
@ -197,7 +197,7 @@ const Panel: FC = () => {
|
|||||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<InUseProviderIcon className='ml-1 h-4' />
|
{InUseProviderIcon && <InUseProviderIcon className='ml-1 h-4' />}
|
||||||
<Divider type='vertical' className='h-3.5' />
|
<Divider type='vertical' className='h-3.5' />
|
||||||
<div className='flex items-center' onClick={e => e.stopPropagation()}>
|
<div className='flex items-center' onClick={e => e.stopPropagation()}>
|
||||||
<ConfigButton
|
<ConfigButton
|
||||||
|
|||||||
@ -439,23 +439,25 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||||||
<Button className='mr-2' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
<Button className='mr-2' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||||
<Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
|
<Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal >
|
|
||||||
{showAppIconPicker && (
|
|
||||||
<AppIconPicker
|
|
||||||
onSelect={(payload) => {
|
|
||||||
setAppIcon(payload)
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setAppIcon(icon_type === 'image'
|
|
||||||
? { type: 'image', url: icon_url!, fileId: icon }
|
|
||||||
: { type: 'emoji', icon, background: icon_background! })
|
|
||||||
setShowAppIconPicker(false)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
|
|
||||||
|
{showAppIconPicker && (
|
||||||
|
<div onClick={e => e.stopPropagation()}>
|
||||||
|
<AppIconPicker
|
||||||
|
onSelect={(payload) => {
|
||||||
|
setAppIcon(payload)
|
||||||
|
setShowAppIconPicker(false)
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setAppIcon(icon_type === 'image'
|
||||||
|
? { type: 'image', url: icon_url!, fileId: icon }
|
||||||
|
: { type: 'emoji', icon, background: icon_background! })
|
||||||
|
setShowAppIconPicker(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default React.memo(SettingsModal)
|
export default React.memo(SettingsModal)
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import {
|
|||||||
} from '@/service/share'
|
} from '@/service/share'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import AnswerIcon from '@/app/components/base/answer-icon'
|
import AnswerIcon from '@/app/components/base/answer-icon'
|
||||||
|
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
|
||||||
|
import { Markdown } from '@/app/components/base/markdown'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
const ChatWrapper = () => {
|
const ChatWrapper = () => {
|
||||||
@ -39,6 +41,10 @@ const ChatWrapper = () => {
|
|||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
appData,
|
appData,
|
||||||
themeBuilder,
|
themeBuilder,
|
||||||
|
sidebarCollapseState,
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
|
setIsResponding,
|
||||||
} = useChatWithHistoryContext()
|
} = useChatWithHistoryContext()
|
||||||
const appConfig = useMemo(() => {
|
const appConfig = useMemo(() => {
|
||||||
const config = appParams || {}
|
const config = appParams || {}
|
||||||
@ -58,7 +64,7 @@ const ChatWrapper = () => {
|
|||||||
setTargetMessageId,
|
setTargetMessageId,
|
||||||
handleSend,
|
handleSend,
|
||||||
handleStop,
|
handleStop,
|
||||||
isResponding,
|
isResponding: respondingState,
|
||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
} = useChat(
|
} = useChat(
|
||||||
appConfig,
|
appConfig,
|
||||||
@ -68,6 +74,8 @@ const ChatWrapper = () => {
|
|||||||
},
|
},
|
||||||
appPrevChatTree,
|
appPrevChatTree,
|
||||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
)
|
)
|
||||||
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
|
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
|
||||||
const inputDisabled = useMemo(() => {
|
const inputDisabled = useMemo(() => {
|
||||||
@ -108,6 +116,10 @@ const ChatWrapper = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsResponding(respondingState)
|
||||||
|
}, [respondingState, setIsResponding])
|
||||||
|
|
||||||
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
|
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
|
||||||
const data: any = {
|
const data: any = {
|
||||||
query: message,
|
query: message,
|
||||||
@ -166,12 +178,33 @@ const ChatWrapper = () => {
|
|||||||
|
|
||||||
const welcome = useMemo(() => {
|
const welcome = useMemo(() => {
|
||||||
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
||||||
|
if (respondingState)
|
||||||
|
return null
|
||||||
if (currentConversationId)
|
if (currentConversationId)
|
||||||
return null
|
return null
|
||||||
if (!welcomeMessage)
|
if (!welcomeMessage)
|
||||||
return null
|
return null
|
||||||
if (!collapsed && inputsForms.length > 0)
|
if (!collapsed && inputsForms.length > 0)
|
||||||
return null
|
return null
|
||||||
|
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className='h-[50vh] py-12 px-4 flex items-center justify-center'>
|
||||||
|
<div className='grow max-w-[720px] flex gap-4'>
|
||||||
|
<AppIcon
|
||||||
|
size='xl'
|
||||||
|
iconType={appData?.site.icon_type}
|
||||||
|
icon={appData?.site.icon}
|
||||||
|
background={appData?.site.icon_background}
|
||||||
|
imageUrl={appData?.site.icon_url}
|
||||||
|
/>
|
||||||
|
<div className='grow px-4 py-3 bg-chat-bubble-bg text-text-primary rounded-2xl body-lg-regular'>
|
||||||
|
<Markdown content={welcomeMessage.content} />
|
||||||
|
<SuggestedQuestions item={welcomeMessage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
|
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
|
||||||
<AppIcon
|
<AppIcon
|
||||||
@ -181,10 +214,10 @@ const ChatWrapper = () => {
|
|||||||
background={appData?.site.icon_background}
|
background={appData?.site.icon_background}
|
||||||
imageUrl={appData?.site.icon_url}
|
imageUrl={appData?.site.icon_url}
|
||||||
/>
|
/>
|
||||||
<div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
|
<Markdown className='!text-text-tertiary !body-2xl-regular' content={welcomeMessage.content} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
|
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState])
|
||||||
|
|
||||||
const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
|
const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
|
||||||
? <AnswerIcon
|
? <AnswerIcon
|
||||||
@ -203,10 +236,10 @@ const ChatWrapper = () => {
|
|||||||
appData={appData}
|
appData={appData}
|
||||||
config={appConfig}
|
config={appConfig}
|
||||||
chatList={messageList}
|
chatList={messageList}
|
||||||
isResponding={isResponding}
|
isResponding={respondingState}
|
||||||
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
|
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[768px] ${isMobile && 'px-4'}`}
|
||||||
chatFooterClassName='pb-4'
|
chatFooterClassName='pb-4'
|
||||||
chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile ? 'px-2' : 'px-4'}`}
|
chatFooterInnerClassName={`mx-auto w-full max-w-[768px] ${isMobile ? 'px-2' : 'px-4'}`}
|
||||||
onSend={doSend}
|
onSend={doSend}
|
||||||
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
|
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
|
||||||
inputsForm={inputsForms}
|
inputsForm={inputsForms}
|
||||||
@ -227,6 +260,7 @@ const ChatWrapper = () => {
|
|||||||
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
|
||||||
inputDisabled={inputDisabled}
|
inputDisabled={inputDisabled}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
sidebarCollapseState={sidebarCollapseState}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -50,6 +50,10 @@ export type ChatWithHistoryContextValue = {
|
|||||||
themeBuilder?: ThemeBuilder
|
themeBuilder?: ThemeBuilder
|
||||||
sidebarCollapseState?: boolean
|
sidebarCollapseState?: boolean
|
||||||
handleSidebarCollapse: (state: boolean) => void
|
handleSidebarCollapse: (state: boolean) => void
|
||||||
|
clearChatList?: boolean
|
||||||
|
setClearChatList: (state: boolean) => void
|
||||||
|
isResponding?: boolean
|
||||||
|
setIsResponding: (state: boolean) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
|
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
|
||||||
@ -77,5 +81,9 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
|
|||||||
currentChatInstanceRef: { current: { handleStop: () => {} } },
|
currentChatInstanceRef: { current: { handleStop: () => {} } },
|
||||||
sidebarCollapseState: false,
|
sidebarCollapseState: false,
|
||||||
handleSidebarCollapse: () => {},
|
handleSidebarCollapse: () => {},
|
||||||
|
clearChatList: false,
|
||||||
|
setClearChatList: () => {},
|
||||||
|
isResponding: false,
|
||||||
|
setIsResponding: () => {},
|
||||||
})
|
})
|
||||||
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)
|
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
useChatWithHistoryContext,
|
useChatWithHistoryContext,
|
||||||
} from '../context'
|
} from '../context'
|
||||||
import Operation from './operation'
|
import Operation from './operation'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown'
|
import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown'
|
||||||
@ -33,6 +33,7 @@ const Header = () => {
|
|||||||
handleNewConversation,
|
handleNewConversation,
|
||||||
sidebarCollapseState,
|
sidebarCollapseState,
|
||||||
handleSidebarCollapse,
|
handleSidebarCollapse,
|
||||||
|
isResponding,
|
||||||
} = useChatWithHistoryContext()
|
} = useChatWithHistoryContext()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isSidebarCollapsed = sidebarCollapseState
|
const isSidebarCollapsed = sidebarCollapseState
|
||||||
@ -106,9 +107,21 @@ const Header = () => {
|
|||||||
<div className='h-[14px] w-px bg-divider-regular'></div>
|
<div className='h-[14px] w-px bg-divider-regular'></div>
|
||||||
</div>
|
</div>
|
||||||
{isSidebarCollapsed && (
|
{isSidebarCollapsed && (
|
||||||
<ActionButton size='l' onClick={handleNewConversation}>
|
<Tooltip
|
||||||
<RiEditBoxLine className='w-[18px] h-[18px]' />
|
disabled={!!currentConversationId}
|
||||||
</ActionButton>
|
popupContent={t('share.chat.newChatTip')}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ActionButton
|
||||||
|
size='l'
|
||||||
|
state={(!currentConversationId || isResponding) ? ActionButtonState.Disabled : ActionButtonState.Default}
|
||||||
|
disabled={!currentConversationId || isResponding}
|
||||||
|
onClick={handleNewConversation}
|
||||||
|
>
|
||||||
|
<RiEditBoxLine className='w-[18px] h-[18px]' />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-1'>
|
<div className='flex items-center gap-1'>
|
||||||
|
|||||||
@ -150,6 +150,8 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
||||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
|
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
|
||||||
|
|
||||||
|
const [clearChatList, setClearChatList] = useState(false)
|
||||||
|
const [isResponding, setIsResponding] = useState(false)
|
||||||
const appPrevChatTree = useMemo(
|
const appPrevChatTree = useMemo(
|
||||||
() => (currentConversationId && appChatListData?.data.length)
|
() => (currentConversationId && appChatListData?.data.length)
|
||||||
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
||||||
@ -310,20 +312,16 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
currentChatInstanceRef.current.handleStop()
|
currentChatInstanceRef.current.handleStop()
|
||||||
setNewConversationId('')
|
setNewConversationId('')
|
||||||
handleConversationIdInfoChange(conversationId)
|
handleConversationIdInfoChange(conversationId)
|
||||||
}, [handleConversationIdInfoChange])
|
if (conversationId)
|
||||||
|
setClearChatList(false)
|
||||||
|
}, [handleConversationIdInfoChange, setClearChatList])
|
||||||
const handleNewConversation = useCallback(() => {
|
const handleNewConversation = useCallback(() => {
|
||||||
currentChatInstanceRef.current.handleStop()
|
currentChatInstanceRef.current.handleStop()
|
||||||
setNewConversationId('')
|
setShowNewConversationItemInList(true)
|
||||||
|
handleChangeConversation('')
|
||||||
if (showNewConversationItemInList) {
|
handleNewConversationInputsChange({})
|
||||||
handleChangeConversation('')
|
setClearChatList(true)
|
||||||
}
|
}, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
|
||||||
else if (currentConversationId) {
|
|
||||||
handleConversationIdInfoChange('')
|
|
||||||
setShowNewConversationItemInList(true)
|
|
||||||
handleNewConversationInputsChange({})
|
|
||||||
}
|
|
||||||
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
|
|
||||||
const handleUpdateConversationList = useCallback(() => {
|
const handleUpdateConversationList = useCallback(() => {
|
||||||
mutateAppConversationData()
|
mutateAppConversationData()
|
||||||
mutateAppPinnedConversationData()
|
mutateAppPinnedConversationData()
|
||||||
@ -462,5 +460,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
sidebarCollapseState,
|
sidebarCollapseState,
|
||||||
handleSidebarCollapse,
|
handleSidebarCollapse,
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
|
isResponding,
|
||||||
|
setIsResponding,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,7 +82,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||||||
{isMobile && (
|
{isMobile && (
|
||||||
<HeaderInMobile />
|
<HeaderInMobile />
|
||||||
)}
|
)}
|
||||||
<div className={cn('relative grow p-2')}>
|
<div className={cn('relative grow p-2', isMobile && 'h-[calc(100%_-_56px)] p-0')}>
|
||||||
{isSidebarCollapsed && (
|
{isSidebarCollapsed && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -95,7 +95,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
|
|||||||
<Sidebar isPanel />
|
<Sidebar isPanel />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='h-full flex flex-col bg-chatbot-bg rounded-2xl border-[0,5px] border-components-panel-border-subtle overflow-hidden'>
|
<div className={cn('h-full flex flex-col bg-chatbot-bg border-[0,5px] border-components-panel-border-subtle overflow-hidden', isMobile ? 'rounded-t-2xl' : 'rounded-2xl')}>
|
||||||
{!isMobile && <Header />}
|
{!isMobile && <Header />}
|
||||||
{appChatListDataLoading && (
|
{appChatListDataLoading && (
|
||||||
<Loading type='app' />
|
<Loading type='app' />
|
||||||
@ -153,6 +153,10 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
|||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
sidebarCollapseState,
|
sidebarCollapseState,
|
||||||
handleSidebarCollapse,
|
handleSidebarCollapse,
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
|
isResponding,
|
||||||
|
setIsResponding,
|
||||||
} = useChatWithHistory(installedAppInfo)
|
} = useChatWithHistory(installedAppInfo)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -190,6 +194,10 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
|
|||||||
themeBuilder,
|
themeBuilder,
|
||||||
sidebarCollapseState,
|
sidebarCollapseState,
|
||||||
handleSidebarCollapse,
|
handleSidebarCollapse,
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
|
isResponding,
|
||||||
|
setIsResponding,
|
||||||
}}>
|
}}>
|
||||||
<ChatWithHistory className={className} />
|
<ChatWithHistory className={className} />
|
||||||
</ChatWithHistoryContext.Provider>
|
</ChatWithHistoryContext.Provider>
|
||||||
|
|||||||
@ -41,6 +41,7 @@ const Sidebar = ({ isPanel }: Props) => {
|
|||||||
sidebarCollapseState,
|
sidebarCollapseState,
|
||||||
handleSidebarCollapse,
|
handleSidebarCollapse,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
isResponding,
|
||||||
} = useChatWithHistoryContext()
|
} = useChatWithHistoryContext()
|
||||||
const isSidebarCollapsed = sidebarCollapseState
|
const isSidebarCollapsed = sidebarCollapseState
|
||||||
|
|
||||||
@ -105,7 +106,7 @@ const Sidebar = ({ isPanel }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='shrink-0 px-3 py-4'>
|
<div className='shrink-0 px-3 py-4'>
|
||||||
<Button variant='secondary-accent' className='w-full justify-center' onClick={handleNewConversation}>
|
<Button variant='secondary-accent' disabled={isResponding} className='w-full justify-center' onClick={handleNewConversation}>
|
||||||
<RiEditBoxLine className='w-4 h-4 mr-1' />
|
<RiEditBoxLine className='w-4 h-4 mr-1' />
|
||||||
{t('share.chat.newChat')}
|
{t('share.chat.newChat')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -110,7 +110,7 @@ const Answer: FC<AnswerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='chat-answer-container group grow w-0 ml-4' ref={containerRef}>
|
<div className='chat-answer-container group grow w-0 ml-4 pb-4' ref={containerRef}>
|
||||||
<div className={cn('group relative pr-10', chatAnswerContainerInner)}>
|
<div className={cn('group relative pr-10', chatAnswerContainerInner)}>
|
||||||
<div
|
<div
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import {
|
import {
|
||||||
RiClipboardLine,
|
RiClipboardLine,
|
||||||
RiEditLine,
|
RiEditLine,
|
||||||
RiReplay15Line,
|
RiResetLeftLine,
|
||||||
RiThumbDownLine,
|
RiThumbDownLine,
|
||||||
RiThumbUpLine,
|
RiThumbUpLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
@ -130,7 +130,7 @@ const Operation: FC<OperationProps> = ({
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
{!noChatInput && (
|
{!noChatInput && (
|
||||||
<ActionButton onClick={() => onRegenerate?.(item)}>
|
<ActionButton onClick={() => onRegenerate?.(item)}>
|
||||||
<RiReplay15Line className='w-4 h-4' />
|
<RiResetLeftLine className='w-4 h-4' />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
{(config?.supportAnnotation && config.annotation_reply?.enabled) && (
|
{(config?.supportAnnotation && config.annotation_reply?.enabled) && (
|
||||||
|
|||||||
@ -51,6 +51,8 @@ export const useChat = (
|
|||||||
},
|
},
|
||||||
prevChatTree?: ChatItemInTree[],
|
prevChatTree?: ChatItemInTree[],
|
||||||
stopChat?: (taskId: string) => void,
|
stopChat?: (taskId: string) => void,
|
||||||
|
clearChatList?: boolean,
|
||||||
|
clearChatListCallback?: (state: boolean) => void,
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { formatTime } = useTimestamp()
|
const { formatTime } = useTimestamp()
|
||||||
@ -90,7 +92,7 @@ export const useChat = (
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ret.unshift({
|
ret.unshift({
|
||||||
id: `${Date.now()}`,
|
id: 'opening-statement',
|
||||||
content: getIntroduction(config.opening_statement),
|
content: getIntroduction(config.opening_statement),
|
||||||
isAnswer: true,
|
isAnswer: true,
|
||||||
isOpeningStatement: true,
|
isOpeningStatement: true,
|
||||||
@ -163,12 +165,13 @@ export const useChat = (
|
|||||||
suggestedQuestionsAbortControllerRef.current.abort()
|
suggestedQuestionsAbortControllerRef.current.abort()
|
||||||
}, [stopChat, handleResponding])
|
}, [stopChat, handleResponding])
|
||||||
|
|
||||||
const handleRestart = useCallback(() => {
|
const handleRestart = useCallback((cb?: any) => {
|
||||||
conversationId.current = ''
|
conversationId.current = ''
|
||||||
taskIdRef.current = ''
|
taskIdRef.current = ''
|
||||||
handleStop()
|
handleStop()
|
||||||
setChatTree([])
|
setChatTree([])
|
||||||
setSuggestQuestions([])
|
setSuggestQuestions([])
|
||||||
|
cb?.()
|
||||||
}, [handleStop])
|
}, [handleStop])
|
||||||
|
|
||||||
const updateCurrentQAOnTree = useCallback(({
|
const updateCurrentQAOnTree = useCallback(({
|
||||||
@ -682,6 +685,11 @@ export const useChat = (
|
|||||||
})
|
})
|
||||||
}, [chatList, updateChatTreeNode])
|
}, [chatList, updateChatTreeNode])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (clearChatList)
|
||||||
|
handleRestart(() => clearChatListCallback?.(false))
|
||||||
|
}, [clearChatList, clearChatListCallback, handleRestart])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chatList,
|
chatList,
|
||||||
setTargetMessageId,
|
setTargetMessageId,
|
||||||
|
|||||||
@ -72,6 +72,7 @@ export type ChatProps = {
|
|||||||
noSpacing?: boolean
|
noSpacing?: boolean
|
||||||
inputDisabled?: boolean
|
inputDisabled?: boolean
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
|
sidebarCollapseState?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Chat: FC<ChatProps> = ({
|
const Chat: FC<ChatProps> = ({
|
||||||
@ -110,6 +111,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
noSpacing,
|
noSpacing,
|
||||||
inputDisabled,
|
inputDisabled,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
sidebarCollapseState,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
|
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
|
||||||
@ -193,6 +195,11 @@ const Chat: FC<ChatProps> = ({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sidebarCollapseState)
|
||||||
|
setTimeout(() => handleWindowResize(), 200)
|
||||||
|
}, [sidebarCollapseState])
|
||||||
|
|
||||||
const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
|
const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -255,7 +262,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-0 bg-chat-input-mask ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`}
|
className={`absolute bottom-0 bg-chat-input-mask flex justify-center ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`}
|
||||||
ref={chatFooterRef}
|
ref={chatFooterRef}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import {
|
|||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
|
import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
|
||||||
import AnswerIcon from '@/app/components/base/answer-icon'
|
import AnswerIcon from '@/app/components/base/answer-icon'
|
||||||
|
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
|
||||||
|
import { Markdown } from '@/app/components/base/markdown'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
const ChatWrapper = () => {
|
const ChatWrapper = () => {
|
||||||
@ -41,6 +43,9 @@ const ChatWrapper = () => {
|
|||||||
handleFeedback,
|
handleFeedback,
|
||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
themeBuilder,
|
themeBuilder,
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
|
setIsResponding,
|
||||||
} = useEmbeddedChatbotContext()
|
} = useEmbeddedChatbotContext()
|
||||||
const appConfig = useMemo(() => {
|
const appConfig = useMemo(() => {
|
||||||
const config = appParams || {}
|
const config = appParams || {}
|
||||||
@ -60,7 +65,7 @@ const ChatWrapper = () => {
|
|||||||
setTargetMessageId,
|
setTargetMessageId,
|
||||||
handleSend,
|
handleSend,
|
||||||
handleStop,
|
handleStop,
|
||||||
isResponding,
|
isResponding: respondingState,
|
||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
} = useChat(
|
} = useChat(
|
||||||
appConfig,
|
appConfig,
|
||||||
@ -70,6 +75,8 @@ const ChatWrapper = () => {
|
|||||||
},
|
},
|
||||||
appPrevChatList,
|
appPrevChatList,
|
||||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
)
|
)
|
||||||
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
|
const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
|
||||||
const inputDisabled = useMemo(() => {
|
const inputDisabled = useMemo(() => {
|
||||||
@ -108,6 +115,9 @@ const ChatWrapper = () => {
|
|||||||
if (currentChatInstanceRef.current)
|
if (currentChatInstanceRef.current)
|
||||||
currentChatInstanceRef.current.handleStop = handleStop
|
currentChatInstanceRef.current.handleStop = handleStop
|
||||||
}, [currentChatInstanceRef, handleStop])
|
}, [currentChatInstanceRef, handleStop])
|
||||||
|
useEffect(() => {
|
||||||
|
setIsResponding(respondingState)
|
||||||
|
}, [respondingState, setIsResponding])
|
||||||
|
|
||||||
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
|
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
|
||||||
const data: any = {
|
const data: any = {
|
||||||
@ -167,12 +177,33 @@ const ChatWrapper = () => {
|
|||||||
|
|
||||||
const welcome = useMemo(() => {
|
const welcome = useMemo(() => {
|
||||||
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
||||||
|
if (respondingState)
|
||||||
|
return null
|
||||||
if (currentConversationId)
|
if (currentConversationId)
|
||||||
return null
|
return null
|
||||||
if (!welcomeMessage)
|
if (!welcomeMessage)
|
||||||
return null
|
return null
|
||||||
if (!collapsed && inputsForms.length > 0)
|
if (!collapsed && inputsForms.length > 0)
|
||||||
return null
|
return null
|
||||||
|
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className='h-[50vh] py-12 px-4 flex items-center justify-center'>
|
||||||
|
<div className='grow max-w-[720px] flex gap-4'>
|
||||||
|
<AppIcon
|
||||||
|
size='xl'
|
||||||
|
iconType={appData?.site.icon_type}
|
||||||
|
icon={appData?.site.icon}
|
||||||
|
background={appData?.site.icon_background}
|
||||||
|
imageUrl={appData?.site.icon_url}
|
||||||
|
/>
|
||||||
|
<div className='grow px-4 py-3 bg-chat-bubble-bg text-text-primary rounded-2xl body-lg-regular'>
|
||||||
|
<Markdown content={welcomeMessage.content} />
|
||||||
|
<SuggestedQuestions item={welcomeMessage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
|
<div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
|
||||||
<AppIcon
|
<AppIcon
|
||||||
@ -182,10 +213,10 @@ const ChatWrapper = () => {
|
|||||||
background={appData?.site.icon_background}
|
background={appData?.site.icon_background}
|
||||||
imageUrl={appData?.site.icon_url}
|
imageUrl={appData?.site.icon_url}
|
||||||
/>
|
/>
|
||||||
<div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
|
<Markdown className='!text-text-tertiary !body-2xl-regular' content={welcomeMessage.content} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
|
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState])
|
||||||
|
|
||||||
const answerIcon = isDify()
|
const answerIcon = isDify()
|
||||||
? <LogoAvatar className='relative shrink-0' />
|
? <LogoAvatar className='relative shrink-0' />
|
||||||
@ -203,10 +234,10 @@ const ChatWrapper = () => {
|
|||||||
appData={appData}
|
appData={appData}
|
||||||
config={appConfig}
|
config={appConfig}
|
||||||
chatList={messageList}
|
chatList={messageList}
|
||||||
isResponding={isResponding}
|
isResponding={respondingState}
|
||||||
chatContainerInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
|
chatContainerInnerClassName={cn('mx-auto w-full max-w-full pt-4 tablet:px-4', isMobile && 'px-4')}
|
||||||
chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')}
|
chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')}
|
||||||
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-2')}
|
chatFooterInnerClassName={cn('mx-auto w-full max-w-full px-4', isMobile && 'px-2')}
|
||||||
onSend={doSend}
|
onSend={doSend}
|
||||||
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
|
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
|
||||||
inputsForm={inputsForms}
|
inputsForm={inputsForms}
|
||||||
|
|||||||
@ -42,6 +42,10 @@ export type EmbeddedChatbotContextValue = {
|
|||||||
handleFeedback: (messageId: string, feedback: Feedback) => void
|
handleFeedback: (messageId: string, feedback: Feedback) => void
|
||||||
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
|
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
|
||||||
themeBuilder?: ThemeBuilder
|
themeBuilder?: ThemeBuilder
|
||||||
|
clearChatList?: boolean
|
||||||
|
setClearChatList: (state: boolean) => void
|
||||||
|
isResponding?: boolean
|
||||||
|
setIsResponding: (state: boolean) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
|
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
|
||||||
@ -62,5 +66,9 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
|
|||||||
isInstalledApp: false,
|
isInstalledApp: false,
|
||||||
handleFeedback: () => {},
|
handleFeedback: () => {},
|
||||||
currentChatInstanceRef: { current: { handleStop: () => {} } },
|
currentChatInstanceRef: { current: { handleStop: () => {} } },
|
||||||
|
clearChatList: false,
|
||||||
|
setClearChatList: () => {},
|
||||||
|
isResponding: false,
|
||||||
|
setIsResponding: () => {},
|
||||||
})
|
})
|
||||||
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)
|
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)
|
||||||
|
|||||||
@ -103,6 +103,8 @@ export const useEmbeddedChatbot = () => {
|
|||||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
||||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
|
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
|
||||||
|
|
||||||
|
const [clearChatList, setClearChatList] = useState(false)
|
||||||
|
const [isResponding, setIsResponding] = useState(false)
|
||||||
const appPrevChatList = useMemo(
|
const appPrevChatList = useMemo(
|
||||||
() => (currentConversationId && appChatListData?.data.length)
|
() => (currentConversationId && appChatListData?.data.length)
|
||||||
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
? buildChatItemTree(getFormattedChatList(appChatListData.data))
|
||||||
@ -283,20 +285,16 @@ export const useEmbeddedChatbot = () => {
|
|||||||
currentChatInstanceRef.current.handleStop()
|
currentChatInstanceRef.current.handleStop()
|
||||||
setNewConversationId('')
|
setNewConversationId('')
|
||||||
handleConversationIdInfoChange(conversationId)
|
handleConversationIdInfoChange(conversationId)
|
||||||
}, [handleConversationIdInfoChange])
|
if (conversationId)
|
||||||
|
setClearChatList(false)
|
||||||
|
}, [handleConversationIdInfoChange, setClearChatList])
|
||||||
const handleNewConversation = useCallback(() => {
|
const handleNewConversation = useCallback(() => {
|
||||||
currentChatInstanceRef.current.handleStop()
|
currentChatInstanceRef.current.handleStop()
|
||||||
setNewConversationId('')
|
setShowNewConversationItemInList(true)
|
||||||
|
handleChangeConversation('')
|
||||||
if (showNewConversationItemInList) {
|
handleNewConversationInputsChange({})
|
||||||
handleChangeConversation('')
|
setClearChatList(true)
|
||||||
}
|
}, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
|
||||||
else if (currentConversationId) {
|
|
||||||
handleConversationIdInfoChange('')
|
|
||||||
setShowNewConversationItemInList(true)
|
|
||||||
handleNewConversationInputsChange({})
|
|
||||||
}
|
|
||||||
}, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
|
|
||||||
|
|
||||||
const handleNewConversationCompleted = useCallback((newConversationId: string) => {
|
const handleNewConversationCompleted = useCallback((newConversationId: string) => {
|
||||||
setNewConversationId(newConversationId)
|
setNewConversationId(newConversationId)
|
||||||
@ -342,5 +340,9 @@ export const useEmbeddedChatbot = () => {
|
|||||||
chatShouldReloadKey,
|
chatShouldReloadKey,
|
||||||
handleFeedback,
|
handleFeedback,
|
||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
|
isResponding,
|
||||||
|
setIsResponding,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -156,6 +156,10 @@ const EmbeddedChatbotWrapper = () => {
|
|||||||
appId,
|
appId,
|
||||||
handleFeedback,
|
handleFeedback,
|
||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
|
isResponding,
|
||||||
|
setIsResponding,
|
||||||
} = useEmbeddedChatbot()
|
} = useEmbeddedChatbot()
|
||||||
|
|
||||||
return <EmbeddedChatbotContext.Provider value={{
|
return <EmbeddedChatbotContext.Provider value={{
|
||||||
@ -185,6 +189,10 @@ const EmbeddedChatbotWrapper = () => {
|
|||||||
handleFeedback,
|
handleFeedback,
|
||||||
currentChatInstanceRef,
|
currentChatInstanceRef,
|
||||||
themeBuilder,
|
themeBuilder,
|
||||||
|
clearChatList,
|
||||||
|
setClearChatList,
|
||||||
|
isResponding,
|
||||||
|
setIsResponding,
|
||||||
}}>
|
}}>
|
||||||
<Chatbot />
|
<Chatbot />
|
||||||
</EmbeddedChatbotContext.Provider>
|
</EmbeddedChatbotContext.Provider>
|
||||||
|
|||||||
@ -46,13 +46,17 @@ function Confirm({
|
|||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape')
|
if (event.key === 'Escape')
|
||||||
onCancel()
|
onCancel()
|
||||||
|
if (event.key === 'Enter' && isShow) {
|
||||||
|
event.preventDefault()
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown)
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown)
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
}
|
}
|
||||||
}, [onCancel])
|
}, [onCancel, onConfirm, isShow])
|
||||||
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (maskClosable && dialogRef.current && !dialogRef.current.contains(event.target as Node))
|
if (maskClosable && dialogRef.current && !dialogRef.current.contains(event.target as Node))
|
||||||
|
|||||||
@ -213,7 +213,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
border: 1px solid var(--color-divider-regular);
|
border: 1px solid var(--color-divider-regular);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
newChat: 'Start New chat',
|
newChat: 'Start New chat',
|
||||||
|
newChatTip: 'Already in a new chat',
|
||||||
chatSettingsTitle: 'New chat setup',
|
chatSettingsTitle: 'New chat setup',
|
||||||
chatFormTip: 'Chat settings cannot be modified after the chat has started.',
|
chatFormTip: 'Chat settings cannot be modified after the chat has started.',
|
||||||
pinnedTitle: 'Pinned',
|
pinnedTitle: 'Pinned',
|
||||||
|
|||||||
@ -6,6 +6,7 @@ const translation = {
|
|||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
newChat: '开启新对话',
|
newChat: '开启新对话',
|
||||||
|
newChatTip: '已在新对话中',
|
||||||
chatSettingsTitle: '新对话设置',
|
chatSettingsTitle: '新对话设置',
|
||||||
chatFormTip: '对话开始后,对话设置将无法修改。',
|
chatFormTip: '对话开始后,对话设置将无法修改。',
|
||||||
pinnedTitle: '已置顶',
|
pinnedTitle: '已置顶',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user