mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-13 20:25:55 +08:00
feat: regenerate in Chat
, agent
and Chatflow
app (#7661)
This commit is contained in:
parent
b32a7713e0
commit
8c51d06222
@ -1 +1,2 @@
|
|||||||
HIDDEN_VALUE = "[__HIDDEN__]"
|
HIDDEN_VALUE = "[__HIDDEN__]"
|
||||||
|
UUID_NIL = "00000000-0000-0000-0000-000000000000"
|
||||||
|
@ -109,6 +109,7 @@ class ChatMessageApi(Resource):
|
|||||||
parser.add_argument("files", type=list, required=False, location="json")
|
parser.add_argument("files", type=list, required=False, location="json")
|
||||||
parser.add_argument("model_config", type=dict, required=True, location="json")
|
parser.add_argument("model_config", type=dict, required=True, location="json")
|
||||||
parser.add_argument("conversation_id", type=uuid_value, location="json")
|
parser.add_argument("conversation_id", type=uuid_value, location="json")
|
||||||
|
parser.add_argument("parent_message_id", type=uuid_value, required=False, location="json")
|
||||||
parser.add_argument("response_mode", type=str, choices=["blocking", "streaming"], location="json")
|
parser.add_argument("response_mode", type=str, choices=["blocking", "streaming"], location="json")
|
||||||
parser.add_argument("retriever_from", type=str, required=False, default="dev", location="json")
|
parser.add_argument("retriever_from", type=str, required=False, default="dev", location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
@ -105,8 +105,6 @@ class ChatMessageListApi(Resource):
|
|||||||
if rest_count > 0:
|
if rest_count > 0:
|
||||||
has_more = True
|
has_more = True
|
||||||
|
|
||||||
history_messages = list(reversed(history_messages))
|
|
||||||
|
|
||||||
return InfiniteScrollPagination(data=history_messages, limit=args["limit"], has_more=has_more)
|
return InfiniteScrollPagination(data=history_messages, limit=args["limit"], has_more=has_more)
|
||||||
|
|
||||||
|
|
||||||
|
@ -166,6 +166,8 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
|
|||||||
parser.add_argument("query", type=str, required=True, location="json", default="")
|
parser.add_argument("query", type=str, required=True, location="json", default="")
|
||||||
parser.add_argument("files", type=list, location="json")
|
parser.add_argument("files", type=list, location="json")
|
||||||
parser.add_argument("conversation_id", type=uuid_value, location="json")
|
parser.add_argument("conversation_id", type=uuid_value, location="json")
|
||||||
|
parser.add_argument("parent_message_id", type=uuid_value, required=False, location="json")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -100,6 +100,7 @@ class ChatApi(InstalledAppResource):
|
|||||||
parser.add_argument("query", type=str, required=True, location="json")
|
parser.add_argument("query", type=str, required=True, location="json")
|
||||||
parser.add_argument("files", type=list, required=False, location="json")
|
parser.add_argument("files", type=list, required=False, location="json")
|
||||||
parser.add_argument("conversation_id", type=uuid_value, location="json")
|
parser.add_argument("conversation_id", type=uuid_value, location="json")
|
||||||
|
parser.add_argument("parent_message_id", type=uuid_value, required=False, location="json")
|
||||||
parser.add_argument("retriever_from", type=str, required=False, default="explore_app", location="json")
|
parser.add_argument("retriever_from", type=str, required=False, default="explore_app", location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ class MessageListApi(InstalledAppResource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return MessageService.pagination_by_first_id(
|
return MessageService.pagination_by_first_id(
|
||||||
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"]
|
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"], "desc"
|
||||||
)
|
)
|
||||||
except services.errors.conversation.ConversationNotExistsError:
|
except services.errors.conversation.ConversationNotExistsError:
|
||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
|
@ -54,6 +54,7 @@ class MessageListApi(Resource):
|
|||||||
message_fields = {
|
message_fields = {
|
||||||
"id": fields.String,
|
"id": fields.String,
|
||||||
"conversation_id": fields.String,
|
"conversation_id": fields.String,
|
||||||
|
"parent_message_id": fields.String,
|
||||||
"inputs": fields.Raw,
|
"inputs": fields.Raw,
|
||||||
"query": fields.String,
|
"query": fields.String,
|
||||||
"answer": fields.String(attribute="re_sign_file_url_answer"),
|
"answer": fields.String(attribute="re_sign_file_url_answer"),
|
||||||
|
@ -96,6 +96,7 @@ class ChatApi(WebApiResource):
|
|||||||
parser.add_argument("files", type=list, required=False, location="json")
|
parser.add_argument("files", type=list, required=False, location="json")
|
||||||
parser.add_argument("response_mode", type=str, choices=["blocking", "streaming"], location="json")
|
parser.add_argument("response_mode", type=str, choices=["blocking", "streaming"], location="json")
|
||||||
parser.add_argument("conversation_id", type=uuid_value, location="json")
|
parser.add_argument("conversation_id", type=uuid_value, location="json")
|
||||||
|
parser.add_argument("parent_message_id", type=uuid_value, required=False, location="json")
|
||||||
parser.add_argument("retriever_from", type=str, required=False, default="web_app", location="json")
|
parser.add_argument("retriever_from", type=str, required=False, default="web_app", location="json")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
@ -57,6 +57,7 @@ class MessageListApi(WebApiResource):
|
|||||||
message_fields = {
|
message_fields = {
|
||||||
"id": fields.String,
|
"id": fields.String,
|
||||||
"conversation_id": fields.String,
|
"conversation_id": fields.String,
|
||||||
|
"parent_message_id": fields.String,
|
||||||
"inputs": fields.Raw,
|
"inputs": fields.Raw,
|
||||||
"query": fields.String,
|
"query": fields.String,
|
||||||
"answer": fields.String(attribute="re_sign_file_url_answer"),
|
"answer": fields.String(attribute="re_sign_file_url_answer"),
|
||||||
@ -89,7 +90,7 @@ class MessageListApi(WebApiResource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return MessageService.pagination_by_first_id(
|
return MessageService.pagination_by_first_id(
|
||||||
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"]
|
app_model, end_user, args["conversation_id"], args["first_id"], args["limit"], "desc"
|
||||||
)
|
)
|
||||||
except services.errors.conversation.ConversationNotExistsError:
|
except services.errors.conversation.ConversationNotExistsError:
|
||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
|
@ -32,6 +32,7 @@ from core.model_runtime.entities.message_entities import (
|
|||||||
from core.model_runtime.entities.model_entities import ModelFeature
|
from core.model_runtime.entities.model_entities import ModelFeature
|
||||||
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
|
||||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
|
from core.prompt.utils.extract_thread_messages import extract_thread_messages
|
||||||
from core.tools.entities.tool_entities import (
|
from core.tools.entities.tool_entities import (
|
||||||
ToolParameter,
|
ToolParameter,
|
||||||
ToolRuntimeVariablePool,
|
ToolRuntimeVariablePool,
|
||||||
@ -441,10 +442,12 @@ class BaseAgentRunner(AppRunner):
|
|||||||
.filter(
|
.filter(
|
||||||
Message.conversation_id == self.message.conversation_id,
|
Message.conversation_id == self.message.conversation_id,
|
||||||
)
|
)
|
||||||
.order_by(Message.created_at.asc())
|
.order_by(Message.created_at.desc())
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
messages = list(reversed(extract_thread_messages(messages)))
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
if message.id == self.message.id:
|
if message.id == self.message.id:
|
||||||
continue
|
continue
|
||||||
|
@ -121,6 +121,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
|||||||
inputs=conversation.inputs if conversation else self._get_cleaned_inputs(inputs, app_config),
|
inputs=conversation.inputs if conversation else self._get_cleaned_inputs(inputs, app_config),
|
||||||
query=query,
|
query=query,
|
||||||
files=file_objs,
|
files=file_objs,
|
||||||
|
parent_message_id=args.get("parent_message_id"),
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
invoke_from=invoke_from,
|
invoke_from=invoke_from,
|
||||||
|
@ -127,6 +127,7 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
|
|||||||
inputs=conversation.inputs if conversation else self._get_cleaned_inputs(inputs, app_config),
|
inputs=conversation.inputs if conversation else self._get_cleaned_inputs(inputs, app_config),
|
||||||
query=query,
|
query=query,
|
||||||
files=file_objs,
|
files=file_objs,
|
||||||
|
parent_message_id=args.get("parent_message_id"),
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
invoke_from=invoke_from,
|
invoke_from=invoke_from,
|
||||||
|
@ -128,6 +128,7 @@ class ChatAppGenerator(MessageBasedAppGenerator):
|
|||||||
inputs=conversation.inputs if conversation else self._get_cleaned_inputs(inputs, app_config),
|
inputs=conversation.inputs if conversation else self._get_cleaned_inputs(inputs, app_config),
|
||||||
query=query,
|
query=query,
|
||||||
files=file_objs,
|
files=file_objs,
|
||||||
|
parent_message_id=args.get("parent_message_id"),
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
invoke_from=invoke_from,
|
invoke_from=invoke_from,
|
||||||
|
@ -218,6 +218,7 @@ class MessageBasedAppGenerator(BaseAppGenerator):
|
|||||||
answer_tokens=0,
|
answer_tokens=0,
|
||||||
answer_unit_price=0,
|
answer_unit_price=0,
|
||||||
answer_price_unit=0,
|
answer_price_unit=0,
|
||||||
|
parent_message_id=getattr(application_generate_entity, "parent_message_id", None),
|
||||||
provider_response_latency=0,
|
provider_response_latency=0,
|
||||||
total_price=0,
|
total_price=0,
|
||||||
currency="USD",
|
currency="USD",
|
||||||
|
@ -122,6 +122,7 @@ class ChatAppGenerateEntity(EasyUIBasedAppGenerateEntity):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
conversation_id: Optional[str] = None
|
conversation_id: Optional[str] = None
|
||||||
|
parent_message_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class CompletionAppGenerateEntity(EasyUIBasedAppGenerateEntity):
|
class CompletionAppGenerateEntity(EasyUIBasedAppGenerateEntity):
|
||||||
@ -138,6 +139,7 @@ class AgentChatAppGenerateEntity(EasyUIBasedAppGenerateEntity):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
conversation_id: Optional[str] = None
|
conversation_id: Optional[str] = None
|
||||||
|
parent_message_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class AdvancedChatAppGenerateEntity(AppGenerateEntity):
|
class AdvancedChatAppGenerateEntity(AppGenerateEntity):
|
||||||
@ -149,6 +151,7 @@ class AdvancedChatAppGenerateEntity(AppGenerateEntity):
|
|||||||
app_config: WorkflowUIBasedAppConfig
|
app_config: WorkflowUIBasedAppConfig
|
||||||
|
|
||||||
conversation_id: Optional[str] = None
|
conversation_id: Optional[str] = None
|
||||||
|
parent_message_id: Optional[str] = None
|
||||||
query: str
|
query: str
|
||||||
|
|
||||||
class SingleIterationRunEntity(BaseModel):
|
class SingleIterationRunEntity(BaseModel):
|
||||||
|
@ -11,6 +11,7 @@ from core.model_runtime.entities.message_entities import (
|
|||||||
TextPromptMessageContent,
|
TextPromptMessageContent,
|
||||||
UserPromptMessage,
|
UserPromptMessage,
|
||||||
)
|
)
|
||||||
|
from core.prompt.utils.extract_thread_messages import extract_thread_messages
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models.model import AppMode, Conversation, Message, MessageFile
|
from models.model import AppMode, Conversation, Message, MessageFile
|
||||||
from models.workflow import WorkflowRun
|
from models.workflow import WorkflowRun
|
||||||
@ -33,8 +34,17 @@ class TokenBufferMemory:
|
|||||||
|
|
||||||
# fetch limited messages, and return reversed
|
# fetch limited messages, and return reversed
|
||||||
query = (
|
query = (
|
||||||
db.session.query(Message.id, Message.query, Message.answer, Message.created_at, Message.workflow_run_id)
|
db.session.query(
|
||||||
.filter(Message.conversation_id == self.conversation.id, Message.answer != "")
|
Message.id,
|
||||||
|
Message.query,
|
||||||
|
Message.answer,
|
||||||
|
Message.created_at,
|
||||||
|
Message.workflow_run_id,
|
||||||
|
Message.parent_message_id,
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
Message.conversation_id == self.conversation.id,
|
||||||
|
)
|
||||||
.order_by(Message.created_at.desc())
|
.order_by(Message.created_at.desc())
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -45,7 +55,12 @@ class TokenBufferMemory:
|
|||||||
|
|
||||||
messages = query.limit(message_limit).all()
|
messages = query.limit(message_limit).all()
|
||||||
|
|
||||||
messages = list(reversed(messages))
|
# instead of all messages from the conversation, we only need to extract messages
|
||||||
|
# that belong to the thread of last message
|
||||||
|
thread_messages = extract_thread_messages(messages)
|
||||||
|
thread_messages.pop(0)
|
||||||
|
messages = list(reversed(thread_messages))
|
||||||
|
|
||||||
message_file_parser = MessageFileParser(tenant_id=app_record.tenant_id, app_id=app_record.id)
|
message_file_parser = MessageFileParser(tenant_id=app_record.tenant_id, app_id=app_record.id)
|
||||||
prompt_messages = []
|
prompt_messages = []
|
||||||
for message in messages:
|
for message in messages:
|
||||||
|
22
api/core/prompt/utils/extract_thread_messages.py
Normal file
22
api/core/prompt/utils/extract_thread_messages.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from constants import UUID_NIL
|
||||||
|
|
||||||
|
|
||||||
|
def extract_thread_messages(messages: list[dict]) -> list[dict]:
|
||||||
|
thread_messages = []
|
||||||
|
next_message = None
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
if not message.parent_message_id:
|
||||||
|
# If the message is regenerated and does not have a parent message, it is the start of a new thread
|
||||||
|
thread_messages.append(message)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not next_message:
|
||||||
|
thread_messages.append(message)
|
||||||
|
next_message = message.parent_message_id
|
||||||
|
else:
|
||||||
|
if next_message in {message.id, UUID_NIL}:
|
||||||
|
thread_messages.append(message)
|
||||||
|
next_message = message.parent_message_id
|
||||||
|
|
||||||
|
return thread_messages
|
@ -75,6 +75,7 @@ message_detail_fields = {
|
|||||||
"metadata": fields.Raw(attribute="message_metadata_dict"),
|
"metadata": fields.Raw(attribute="message_metadata_dict"),
|
||||||
"status": fields.String,
|
"status": fields.String,
|
||||||
"error": fields.String,
|
"error": fields.String,
|
||||||
|
"parent_message_id": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
feedback_stat_fields = {"like": fields.Integer, "dislike": fields.Integer}
|
feedback_stat_fields = {"like": fields.Integer, "dislike": fields.Integer}
|
||||||
|
@ -62,6 +62,7 @@ retriever_resource_fields = {
|
|||||||
message_fields = {
|
message_fields = {
|
||||||
"id": fields.String,
|
"id": fields.String,
|
||||||
"conversation_id": fields.String,
|
"conversation_id": fields.String,
|
||||||
|
"parent_message_id": fields.String,
|
||||||
"inputs": fields.Raw,
|
"inputs": fields.Raw,
|
||||||
"query": fields.String,
|
"query": fields.String,
|
||||||
"answer": fields.String(attribute="re_sign_file_url_answer"),
|
"answer": fields.String(attribute="re_sign_file_url_answer"),
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
"""add parent_message_id to messages
|
||||||
|
|
||||||
|
Revision ID: d57ba9ebb251
|
||||||
|
Revises: 675b5321501b
|
||||||
|
Create Date: 2024-09-11 10:12:45.826265
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
import models as models
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd57ba9ebb251'
|
||||||
|
down_revision = '675b5321501b'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('messages', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('parent_message_id', models.types.StringUUID(), nullable=True))
|
||||||
|
|
||||||
|
# Set parent_message_id for existing messages to uuid_nil() to distinguish them from new messages with actual parent IDs or NULLs
|
||||||
|
op.execute('UPDATE messages SET parent_message_id = uuid_nil() WHERE parent_message_id IS NULL')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('messages', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('parent_message_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
@ -710,6 +710,7 @@ class Message(db.Model):
|
|||||||
answer_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
|
answer_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
|
||||||
answer_unit_price = db.Column(db.Numeric(10, 4), nullable=False)
|
answer_unit_price = db.Column(db.Numeric(10, 4), nullable=False)
|
||||||
answer_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001"))
|
answer_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001"))
|
||||||
|
parent_message_id = db.Column(StringUUID, nullable=True)
|
||||||
provider_response_latency = db.Column(db.Float, nullable=False, server_default=db.text("0"))
|
provider_response_latency = db.Column(db.Float, nullable=False, server_default=db.text("0"))
|
||||||
total_price = db.Column(db.Numeric(10, 7))
|
total_price = db.Column(db.Numeric(10, 7))
|
||||||
currency = db.Column(db.String(255), nullable=False)
|
currency = db.Column(db.String(255), nullable=False)
|
||||||
|
@ -34,6 +34,7 @@ class MessageService:
|
|||||||
conversation_id: str,
|
conversation_id: str,
|
||||||
first_id: Optional[str],
|
first_id: Optional[str],
|
||||||
limit: int,
|
limit: int,
|
||||||
|
order: str = "asc",
|
||||||
) -> InfiniteScrollPagination:
|
) -> InfiniteScrollPagination:
|
||||||
if not user:
|
if not user:
|
||||||
return InfiniteScrollPagination(data=[], limit=limit, has_more=False)
|
return InfiniteScrollPagination(data=[], limit=limit, has_more=False)
|
||||||
@ -91,7 +92,8 @@ class MessageService:
|
|||||||
if rest_count > 0:
|
if rest_count > 0:
|
||||||
has_more = True
|
has_more = True
|
||||||
|
|
||||||
history_messages = list(reversed(history_messages))
|
if order == "asc":
|
||||||
|
history_messages = list(reversed(history_messages))
|
||||||
|
|
||||||
return InfiniteScrollPagination(data=history_messages, limit=limit, has_more=has_more)
|
return InfiniteScrollPagination(data=history_messages, limit=limit, has_more=has_more)
|
||||||
|
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from constants import UUID_NIL
|
||||||
|
from core.prompt.utils.extract_thread_messages import extract_thread_messages
|
||||||
|
|
||||||
|
|
||||||
|
class TestMessage:
|
||||||
|
def __init__(self, id, parent_message_id):
|
||||||
|
self.id = id
|
||||||
|
self.parent_message_id = parent_message_id
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
return getattr(self, item)
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_thread_messages_single_message():
|
||||||
|
messages = [TestMessage(str(uuid4()), UUID_NIL)]
|
||||||
|
result = extract_thread_messages(messages)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == messages[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_thread_messages_linear_thread():
|
||||||
|
id1, id2, id3, id4, id5 = str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4())
|
||||||
|
messages = [
|
||||||
|
TestMessage(id5, id4),
|
||||||
|
TestMessage(id4, id3),
|
||||||
|
TestMessage(id3, id2),
|
||||||
|
TestMessage(id2, id1),
|
||||||
|
TestMessage(id1, UUID_NIL),
|
||||||
|
]
|
||||||
|
result = extract_thread_messages(messages)
|
||||||
|
assert len(result) == 5
|
||||||
|
assert [msg["id"] for msg in result] == [id5, id4, id3, id2, id1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_thread_messages_branched_thread():
|
||||||
|
id1, id2, id3, id4 = str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4())
|
||||||
|
messages = [
|
||||||
|
TestMessage(id4, id2),
|
||||||
|
TestMessage(id3, id2),
|
||||||
|
TestMessage(id2, id1),
|
||||||
|
TestMessage(id1, UUID_NIL),
|
||||||
|
]
|
||||||
|
result = extract_thread_messages(messages)
|
||||||
|
assert len(result) == 3
|
||||||
|
assert [msg["id"] for msg in result] == [id4, id2, id1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_thread_messages_empty_list():
|
||||||
|
messages = []
|
||||||
|
result = extract_thread_messages(messages)
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_thread_messages_partially_loaded():
|
||||||
|
id0, id1, id2, id3 = str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4())
|
||||||
|
messages = [
|
||||||
|
TestMessage(id3, id2),
|
||||||
|
TestMessage(id2, id1),
|
||||||
|
TestMessage(id1, id0),
|
||||||
|
]
|
||||||
|
result = extract_thread_messages(messages)
|
||||||
|
assert len(result) == 3
|
||||||
|
assert [msg["id"] for msg in result] == [id3, id2, id1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_thread_messages_legacy_messages():
|
||||||
|
id1, id2, id3 = str(uuid4()), str(uuid4()), str(uuid4())
|
||||||
|
messages = [
|
||||||
|
TestMessage(id3, UUID_NIL),
|
||||||
|
TestMessage(id2, UUID_NIL),
|
||||||
|
TestMessage(id1, UUID_NIL),
|
||||||
|
]
|
||||||
|
result = extract_thread_messages(messages)
|
||||||
|
assert len(result) == 3
|
||||||
|
assert [msg["id"] for msg in result] == [id3, id2, id1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_thread_messages_mixed_with_legacy_messages():
|
||||||
|
id1, id2, id3, id4, id5 = str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4())
|
||||||
|
messages = [
|
||||||
|
TestMessage(id5, id4),
|
||||||
|
TestMessage(id4, id2),
|
||||||
|
TestMessage(id3, id2),
|
||||||
|
TestMessage(id2, UUID_NIL),
|
||||||
|
TestMessage(id1, UUID_NIL),
|
||||||
|
]
|
||||||
|
result = extract_thread_messages(messages)
|
||||||
|
assert len(result) == 4
|
||||||
|
assert [msg["id"] for msg in result] == [id5, id4, id2, id1]
|
@ -46,6 +46,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
|||||||
const config = useConfigFromDebugContext()
|
const config = useConfigFromDebugContext()
|
||||||
const {
|
const {
|
||||||
chatList,
|
chatList,
|
||||||
|
chatListRef,
|
||||||
isResponding,
|
isResponding,
|
||||||
handleSend,
|
handleSend,
|
||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
@ -80,6 +81,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
|||||||
query: message,
|
query: message,
|
||||||
inputs,
|
inputs,
|
||||||
model_config: configData,
|
model_config: configData,
|
||||||
|
parent_message_id: chatListRef.current.at(-1)?.id || null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visionConfig.enabled && files?.length && supportVision)
|
if (visionConfig.enabled && files?.length && supportVision)
|
||||||
@ -93,7 +95,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
|||||||
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled])
|
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled, chatListRef])
|
||||||
|
|
||||||
const { eventEmitter } = useEventEmitterContextContext()
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
eventEmitter?.useSubscription((v: any) => {
|
eventEmitter?.useSubscription((v: any) => {
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
import Chat from '@/app/components/base/chat/chat'
|
import Chat from '@/app/components/base/chat/chat'
|
||||||
import { useChat } from '@/app/components/base/chat/chat/hooks'
|
import { useChat } from '@/app/components/base/chat/chat/hooks'
|
||||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||||
import type { OnSend } from '@/app/components/base/chat/types'
|
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import {
|
import {
|
||||||
fetchConversationMessages,
|
fetchConversationMessages,
|
||||||
@ -45,10 +45,12 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||||||
const config = useConfigFromDebugContext()
|
const config = useConfigFromDebugContext()
|
||||||
const {
|
const {
|
||||||
chatList,
|
chatList,
|
||||||
|
chatListRef,
|
||||||
isResponding,
|
isResponding,
|
||||||
handleSend,
|
handleSend,
|
||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
handleStop,
|
handleStop,
|
||||||
|
handleUpdateChatList,
|
||||||
handleRestart,
|
handleRestart,
|
||||||
handleAnnotationAdded,
|
handleAnnotationAdded,
|
||||||
handleAnnotationEdited,
|
handleAnnotationEdited,
|
||||||
@ -64,7 +66,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||||||
)
|
)
|
||||||
useFormattingChangedSubscription(chatList)
|
useFormattingChangedSubscription(chatList)
|
||||||
|
|
||||||
const doSend: OnSend = useCallback((message, files) => {
|
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||||
if (checkCanSend && !checkCanSend())
|
if (checkCanSend && !checkCanSend())
|
||||||
return
|
return
|
||||||
const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider)
|
const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider)
|
||||||
@ -85,6 +87,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||||||
query: message,
|
query: message,
|
||||||
inputs,
|
inputs,
|
||||||
model_config: configData,
|
model_config: configData,
|
||||||
|
parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visionConfig.enabled && files?.length && supportVision)
|
if (visionConfig.enabled && files?.length && supportVision)
|
||||||
@ -98,7 +101,23 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||||||
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled])
|
}, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled])
|
||||||
|
|
||||||
|
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||||
|
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||||
|
if (index === -1)
|
||||||
|
return
|
||||||
|
|
||||||
|
const prevMessages = chatList.slice(0, index)
|
||||||
|
const question = prevMessages.pop()
|
||||||
|
const lastAnswer = prevMessages.at(-1)
|
||||||
|
|
||||||
|
if (!question)
|
||||||
|
return
|
||||||
|
|
||||||
|
handleUpdateChatList(prevMessages)
|
||||||
|
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||||
|
}, [chatList, handleUpdateChatList, doSend])
|
||||||
|
|
||||||
const allToolIcons = useMemo(() => {
|
const allToolIcons = useMemo(() => {
|
||||||
const icons: Record<string, any> = {}
|
const icons: Record<string, any> = {}
|
||||||
@ -123,6 +142,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||||||
chatFooterClassName='px-6 pt-10 pb-4'
|
chatFooterClassName='px-6 pt-10 pb-4'
|
||||||
suggestedQuestions={suggestedQuestions}
|
suggestedQuestions={suggestedQuestions}
|
||||||
onSend={doSend}
|
onSend={doSend}
|
||||||
|
onRegenerate={doRegenerate}
|
||||||
onStopResponding={handleStop}
|
onStopResponding={handleStop}
|
||||||
showPromptLog
|
showPromptLog
|
||||||
questionIcon={<Avatar name={userProfile.name} size={40} />}
|
questionIcon={<Avatar name={userProfile.name} size={40} />}
|
||||||
|
@ -16,6 +16,7 @@ import timezone from 'dayjs/plugin/timezone'
|
|||||||
import { createContext, useContext } from 'use-context-selector'
|
import { createContext, useContext } from 'use-context-selector'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { UUID_NIL } from '../../base/chat/constants'
|
||||||
import s from './style.module.css'
|
import s from './style.module.css'
|
||||||
import VarPanel from './var-panel'
|
import VarPanel from './var-panel'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
@ -81,72 +82,92 @@ const PARAM_MAP = {
|
|||||||
frequency_penalty: 'Frequency Penalty',
|
frequency_penalty: 'Frequency Penalty',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format interface data for easy display
|
function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId: string, timezone: string, format: string) {
|
||||||
|
newChatList.push({
|
||||||
|
id: item.id,
|
||||||
|
content: item.answer,
|
||||||
|
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||||
|
feedback: item.feedbacks.find((item: any) => item.from_source === 'user'), // user feedback
|
||||||
|
adminFeedback: item.feedbacks.find((item: any) => item.from_source === 'admin'), // admin feedback
|
||||||
|
feedbackDisabled: false,
|
||||||
|
isAnswer: true,
|
||||||
|
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||||
|
log: [
|
||||||
|
...item.message,
|
||||||
|
...(item.message[item.message.length - 1]?.role !== 'assistant'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
text: item.answer,
|
||||||
|
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
workflow_run_id: item.workflow_run_id,
|
||||||
|
conversationId,
|
||||||
|
input: {
|
||||||
|
inputs: item.inputs,
|
||||||
|
query: item.query,
|
||||||
|
},
|
||||||
|
more: {
|
||||||
|
time: dayjs.unix(item.created_at).tz(timezone).format(format),
|
||||||
|
tokens: item.answer_tokens + item.message_tokens,
|
||||||
|
latency: item.provider_response_latency.toFixed(2),
|
||||||
|
},
|
||||||
|
citation: item.metadata?.retriever_resources,
|
||||||
|
annotation: (() => {
|
||||||
|
if (item.annotation_hit_history) {
|
||||||
|
return {
|
||||||
|
id: item.annotation_hit_history.annotation_id,
|
||||||
|
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
|
||||||
|
created_at: item.annotation_hit_history.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.annotation) {
|
||||||
|
return {
|
||||||
|
id: item.annotation.id,
|
||||||
|
authorName: item.annotation.account.name,
|
||||||
|
logAnnotation: item.annotation,
|
||||||
|
created_at: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
})(),
|
||||||
|
parentMessageId: `question-${item.id}`,
|
||||||
|
})
|
||||||
|
newChatList.push({
|
||||||
|
id: `question-${item.id}`,
|
||||||
|
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
|
||||||
|
isAnswer: false,
|
||||||
|
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||||
|
parentMessageId: item.parent_message_id || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
|
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
|
||||||
const newChatList: IChatItem[] = []
|
const newChatList: IChatItem[] = []
|
||||||
messages.forEach((item: ChatMessage) => {
|
let nextMessageId = null
|
||||||
newChatList.push({
|
for (const item of messages) {
|
||||||
id: `question-${item.id}`,
|
if (!item.parent_message_id) {
|
||||||
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
|
appendQAToChatList(newChatList, item, conversationId, timezone, format)
|
||||||
isAnswer: false,
|
break
|
||||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
}
|
||||||
})
|
|
||||||
newChatList.push({
|
|
||||||
id: item.id,
|
|
||||||
content: item.answer,
|
|
||||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
|
||||||
feedback: item.feedbacks.find(item => item.from_source === 'user'), // user feedback
|
|
||||||
adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
|
|
||||||
feedbackDisabled: false,
|
|
||||||
isAnswer: true,
|
|
||||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
|
||||||
log: [
|
|
||||||
...item.message,
|
|
||||||
...(item.message[item.message.length - 1]?.role !== 'assistant'
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
text: item.answer,
|
|
||||||
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
workflow_run_id: item.workflow_run_id,
|
|
||||||
conversationId,
|
|
||||||
input: {
|
|
||||||
inputs: item.inputs,
|
|
||||||
query: item.query,
|
|
||||||
},
|
|
||||||
more: {
|
|
||||||
time: dayjs.unix(item.created_at).tz(timezone).format(format),
|
|
||||||
tokens: item.answer_tokens + item.message_tokens,
|
|
||||||
latency: item.provider_response_latency.toFixed(2),
|
|
||||||
},
|
|
||||||
citation: item.metadata?.retriever_resources,
|
|
||||||
annotation: (() => {
|
|
||||||
if (item.annotation_hit_history) {
|
|
||||||
return {
|
|
||||||
id: item.annotation_hit_history.annotation_id,
|
|
||||||
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
|
|
||||||
created_at: item.annotation_hit_history.created_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.annotation) {
|
if (!nextMessageId) {
|
||||||
return {
|
appendQAToChatList(newChatList, item, conversationId, timezone, format)
|
||||||
id: item.annotation.id,
|
nextMessageId = item.parent_message_id
|
||||||
authorName: item.annotation.account.name,
|
}
|
||||||
logAnnotation: item.annotation,
|
else {
|
||||||
created_at: 0,
|
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||||
}
|
appendQAToChatList(newChatList, item, conversationId, timezone, format)
|
||||||
}
|
nextMessageId = item.parent_message_id
|
||||||
|
}
|
||||||
return undefined
|
}
|
||||||
})(),
|
}
|
||||||
})
|
return newChatList.reverse()
|
||||||
})
|
|
||||||
return newChatList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// const displayedParams = CompletionParams.slice(0, -2)
|
// const displayedParams = CompletionParams.slice(0, -2)
|
||||||
@ -171,6 +192,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||||||
})))
|
})))
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [items, setItems] = React.useState<IChatItem[]>([])
|
const [items, setItems] = React.useState<IChatItem[]>([])
|
||||||
|
const fetchedMessages = useRef<ChatMessage[]>([])
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [varValues, setVarValues] = useState<Record<string, string>>({})
|
const [varValues, setVarValues] = useState<Record<string, string>>({})
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@ -192,7 +214,8 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||||||
const varValues = messageRes.data[0].inputs
|
const varValues = messageRes.data[0].inputs
|
||||||
setVarValues(varValues)
|
setVarValues(varValues)
|
||||||
}
|
}
|
||||||
const newItems = [...getFormattedChatList(messageRes.data, detail.id, timezone!, t('appLog.dateTimeFormat') as string), ...items]
|
fetchedMessages.current = [...fetchedMessages.current, ...messageRes.data]
|
||||||
|
const newItems = getFormattedChatList(fetchedMessages.current, detail.id, timezone!, t('appLog.dateTimeFormat') as string)
|
||||||
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
|
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
|
||||||
newItems.unshift({
|
newItems.unshift({
|
||||||
id: 'introduction',
|
id: 'introduction',
|
||||||
@ -435,7 +458,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||||||
siteInfo={null}
|
siteInfo={null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
: items.length < 8
|
: (items.length < 8 && !hasMore)
|
||||||
? <div className="pt-4 mb-4">
|
? <div className="pt-4 mb-4">
|
||||||
<Chat
|
<Chat
|
||||||
config={{
|
config={{
|
||||||
|
@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react'
|
|||||||
import Chat from '../chat'
|
import Chat from '../chat'
|
||||||
import type {
|
import type {
|
||||||
ChatConfig,
|
ChatConfig,
|
||||||
|
ChatItem,
|
||||||
OnSend,
|
OnSend,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { useChat } from '../chat/hooks'
|
import { useChat } from '../chat/hooks'
|
||||||
@ -44,6 +45,8 @@ const ChatWrapper = () => {
|
|||||||
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
||||||
const {
|
const {
|
||||||
chatList,
|
chatList,
|
||||||
|
chatListRef,
|
||||||
|
handleUpdateChatList,
|
||||||
handleSend,
|
handleSend,
|
||||||
handleStop,
|
handleStop,
|
||||||
isResponding,
|
isResponding,
|
||||||
@ -63,11 +66,12 @@ const ChatWrapper = () => {
|
|||||||
currentChatInstanceRef.current.handleStop = handleStop
|
currentChatInstanceRef.current.handleStop = handleStop
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const doSend: OnSend = useCallback((message, files) => {
|
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||||
const data: any = {
|
const data: any = {
|
||||||
query: message,
|
query: message,
|
||||||
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
||||||
conversation_id: currentConversationId,
|
conversation_id: currentConversationId,
|
||||||
|
parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appConfig?.file_upload?.image.enabled && files?.length)
|
if (appConfig?.file_upload?.image.enabled && files?.length)
|
||||||
@ -83,6 +87,7 @@ const ChatWrapper = () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
|
chatListRef,
|
||||||
appConfig,
|
appConfig,
|
||||||
currentConversationId,
|
currentConversationId,
|
||||||
currentConversationItem,
|
currentConversationItem,
|
||||||
@ -92,6 +97,23 @@ const ChatWrapper = () => {
|
|||||||
isInstalledApp,
|
isInstalledApp,
|
||||||
appId,
|
appId,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||||
|
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||||
|
if (index === -1)
|
||||||
|
return
|
||||||
|
|
||||||
|
const prevMessages = chatList.slice(0, index)
|
||||||
|
const question = prevMessages.pop()
|
||||||
|
const lastAnswer = prevMessages.at(-1)
|
||||||
|
|
||||||
|
if (!question)
|
||||||
|
return
|
||||||
|
|
||||||
|
handleUpdateChatList(prevMessages)
|
||||||
|
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||||
|
}, [chatList, handleUpdateChatList, doSend])
|
||||||
|
|
||||||
const chatNode = useMemo(() => {
|
const chatNode = useMemo(() => {
|
||||||
if (inputsForms.length) {
|
if (inputsForms.length) {
|
||||||
return (
|
return (
|
||||||
@ -148,6 +170,7 @@ const ChatWrapper = () => {
|
|||||||
chatFooterClassName='pb-4'
|
chatFooterClassName='pb-4'
|
||||||
chatFooterInnerClassName={`mx-auto w-full max-w-full ${isMobile && 'px-4'}`}
|
chatFooterInnerClassName={`mx-auto w-full max-w-full ${isMobile && 'px-4'}`}
|
||||||
onSend={doSend}
|
onSend={doSend}
|
||||||
|
onRegenerate={doRegenerate}
|
||||||
onStopResponding={handleStop}
|
onStopResponding={handleStop}
|
||||||
chatNode={chatNode}
|
chatNode={chatNode}
|
||||||
allToolIcons={appMeta?.tool_icons || {}}
|
allToolIcons={appMeta?.tool_icons || {}}
|
||||||
|
@ -12,10 +12,10 @@ import produce from 'immer'
|
|||||||
import type {
|
import type {
|
||||||
Callback,
|
Callback,
|
||||||
ChatConfig,
|
ChatConfig,
|
||||||
ChatItem,
|
|
||||||
Feedback,
|
Feedback,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { CONVERSATION_ID_INFO } from '../constants'
|
import { CONVERSATION_ID_INFO } from '../constants'
|
||||||
|
import { getPrevChatList } from '../utils'
|
||||||
import {
|
import {
|
||||||
delConversation,
|
delConversation,
|
||||||
fetchAppInfo,
|
fetchAppInfo,
|
||||||
@ -34,7 +34,6 @@ import type {
|
|||||||
AppData,
|
AppData,
|
||||||
ConversationItem,
|
ConversationItem,
|
||||||
} from '@/models/share'
|
} from '@/models/share'
|
||||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
import { changeLanguage } from '@/i18n/i18next-config'
|
import { changeLanguage } from '@/i18n/i18next-config'
|
||||||
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||||
@ -108,32 +107,12 @@ 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 appPrevChatList = useMemo(() => {
|
const appPrevChatList = useMemo(
|
||||||
const data = appChatListData?.data || []
|
() => (currentConversationId && appChatListData?.data.length)
|
||||||
const chatList: ChatItem[] = []
|
? getPrevChatList(appChatListData.data)
|
||||||
|
: [],
|
||||||
if (currentConversationId && data.length) {
|
[appChatListData, currentConversationId],
|
||||||
data.forEach((item: any) => {
|
)
|
||||||
chatList.push({
|
|
||||||
id: `question-${item.id}`,
|
|
||||||
content: item.query,
|
|
||||||
isAnswer: false,
|
|
||||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
|
||||||
})
|
|
||||||
chatList.push({
|
|
||||||
id: item.id,
|
|
||||||
content: item.answer,
|
|
||||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
|
||||||
feedback: item.feedback,
|
|
||||||
isAnswer: true,
|
|
||||||
citation: item.retriever_resources,
|
|
||||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return chatList
|
|
||||||
}, [appChatListData, currentConversationId])
|
|
||||||
|
|
||||||
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
|
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ type AnswerProps = {
|
|||||||
chatAnswerContainerInner?: string
|
chatAnswerContainerInner?: string
|
||||||
hideProcessDetail?: boolean
|
hideProcessDetail?: boolean
|
||||||
appData?: AppData
|
appData?: AppData
|
||||||
|
noChatInput?: boolean
|
||||||
}
|
}
|
||||||
const Answer: FC<AnswerProps> = ({
|
const Answer: FC<AnswerProps> = ({
|
||||||
item,
|
item,
|
||||||
@ -48,6 +49,7 @@ const Answer: FC<AnswerProps> = ({
|
|||||||
chatAnswerContainerInner,
|
chatAnswerContainerInner,
|
||||||
hideProcessDetail,
|
hideProcessDetail,
|
||||||
appData,
|
appData,
|
||||||
|
noChatInput,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
@ -110,6 +112,7 @@ const Answer: FC<AnswerProps> = ({
|
|||||||
question={question}
|
question={question}
|
||||||
index={index}
|
index={index}
|
||||||
showPromptLog={showPromptLog}
|
showPromptLog={showPromptLog}
|
||||||
|
noChatInput={noChatInput}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { ChatItem } from '../../types'
|
import type { ChatItem } from '../../types'
|
||||||
import { useChatContext } from '../context'
|
import { useChatContext } from '../context'
|
||||||
|
import RegenerateBtn from '@/app/components/base/regenerate-btn'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import CopyBtn from '@/app/components/base/copy-btn'
|
import CopyBtn from '@/app/components/base/copy-btn'
|
||||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||||
@ -28,6 +29,7 @@ type OperationProps = {
|
|||||||
maxSize: number
|
maxSize: number
|
||||||
contentWidth: number
|
contentWidth: number
|
||||||
hasWorkflowProcess: boolean
|
hasWorkflowProcess: boolean
|
||||||
|
noChatInput?: boolean
|
||||||
}
|
}
|
||||||
const Operation: FC<OperationProps> = ({
|
const Operation: FC<OperationProps> = ({
|
||||||
item,
|
item,
|
||||||
@ -37,6 +39,7 @@ const Operation: FC<OperationProps> = ({
|
|||||||
maxSize,
|
maxSize,
|
||||||
contentWidth,
|
contentWidth,
|
||||||
hasWorkflowProcess,
|
hasWorkflowProcess,
|
||||||
|
noChatInput,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
@ -45,6 +48,7 @@ const Operation: FC<OperationProps> = ({
|
|||||||
onAnnotationEdited,
|
onAnnotationEdited,
|
||||||
onAnnotationRemoved,
|
onAnnotationRemoved,
|
||||||
onFeedback,
|
onFeedback,
|
||||||
|
onRegenerate,
|
||||||
} = useChatContext()
|
} = useChatContext()
|
||||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||||
const {
|
const {
|
||||||
@ -159,12 +163,13 @@ const Operation: FC<OperationProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
!isOpeningStatement && !noChatInput && <RegenerateBtn className='hidden group-hover:block mr-1' onClick={() => onRegenerate?.(item)} />
|
||||||
|
}
|
||||||
{
|
{
|
||||||
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||||
<div className='hidden group-hover:flex ml-1 shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
|
<div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
|
||||||
<Tooltip
|
<Tooltip popupContent={t('appDebug.operation.agree')}>
|
||||||
popupContent={t('appDebug.operation.agree')}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||||
onClick={() => handleFeedback('like')}
|
onClick={() => handleFeedback('like')}
|
||||||
|
@ -12,6 +12,7 @@ export type ChatContextValue = Pick<ChatProps, 'config'
|
|||||||
| 'answerIcon'
|
| 'answerIcon'
|
||||||
| 'allToolIcons'
|
| 'allToolIcons'
|
||||||
| 'onSend'
|
| 'onSend'
|
||||||
|
| 'onRegenerate'
|
||||||
| 'onAnnotationEdited'
|
| 'onAnnotationEdited'
|
||||||
| 'onAnnotationAdded'
|
| 'onAnnotationAdded'
|
||||||
| 'onAnnotationRemoved'
|
| 'onAnnotationRemoved'
|
||||||
@ -36,6 +37,7 @@ export const ChatContextProvider = ({
|
|||||||
answerIcon,
|
answerIcon,
|
||||||
allToolIcons,
|
allToolIcons,
|
||||||
onSend,
|
onSend,
|
||||||
|
onRegenerate,
|
||||||
onAnnotationEdited,
|
onAnnotationEdited,
|
||||||
onAnnotationAdded,
|
onAnnotationAdded,
|
||||||
onAnnotationRemoved,
|
onAnnotationRemoved,
|
||||||
@ -51,6 +53,7 @@ export const ChatContextProvider = ({
|
|||||||
answerIcon,
|
answerIcon,
|
||||||
allToolIcons,
|
allToolIcons,
|
||||||
onSend,
|
onSend,
|
||||||
|
onRegenerate,
|
||||||
onAnnotationEdited,
|
onAnnotationEdited,
|
||||||
onAnnotationAdded,
|
onAnnotationAdded,
|
||||||
onAnnotationRemoved,
|
onAnnotationRemoved,
|
||||||
|
@ -647,7 +647,8 @@ export const useChat = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
chatList,
|
chatList,
|
||||||
setChatList,
|
chatListRef,
|
||||||
|
handleUpdateChatList,
|
||||||
conversationId: conversationId.current,
|
conversationId: conversationId.current,
|
||||||
isResponding,
|
isResponding,
|
||||||
setIsResponding,
|
setIsResponding,
|
||||||
|
@ -16,6 +16,7 @@ import type {
|
|||||||
ChatConfig,
|
ChatConfig,
|
||||||
ChatItem,
|
ChatItem,
|
||||||
Feedback,
|
Feedback,
|
||||||
|
OnRegenerate,
|
||||||
OnSend,
|
OnSend,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
|
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
|
||||||
@ -42,6 +43,7 @@ export type ChatProps = {
|
|||||||
onStopResponding?: () => void
|
onStopResponding?: () => void
|
||||||
noChatInput?: boolean
|
noChatInput?: boolean
|
||||||
onSend?: OnSend
|
onSend?: OnSend
|
||||||
|
onRegenerate?: OnRegenerate
|
||||||
chatContainerClassName?: string
|
chatContainerClassName?: string
|
||||||
chatContainerInnerClassName?: string
|
chatContainerInnerClassName?: string
|
||||||
chatFooterClassName?: string
|
chatFooterClassName?: string
|
||||||
@ -67,6 +69,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
appData,
|
appData,
|
||||||
config,
|
config,
|
||||||
onSend,
|
onSend,
|
||||||
|
onRegenerate,
|
||||||
chatList,
|
chatList,
|
||||||
isResponding,
|
isResponding,
|
||||||
noStopResponding,
|
noStopResponding,
|
||||||
@ -186,6 +189,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
answerIcon={answerIcon}
|
answerIcon={answerIcon}
|
||||||
allToolIcons={allToolIcons}
|
allToolIcons={allToolIcons}
|
||||||
onSend={onSend}
|
onSend={onSend}
|
||||||
|
onRegenerate={onRegenerate}
|
||||||
onAnnotationAdded={onAnnotationAdded}
|
onAnnotationAdded={onAnnotationAdded}
|
||||||
onAnnotationEdited={onAnnotationEdited}
|
onAnnotationEdited={onAnnotationEdited}
|
||||||
onAnnotationRemoved={onAnnotationRemoved}
|
onAnnotationRemoved={onAnnotationRemoved}
|
||||||
@ -219,6 +223,7 @@ const Chat: FC<ChatProps> = ({
|
|||||||
showPromptLog={showPromptLog}
|
showPromptLog={showPromptLog}
|
||||||
chatAnswerContainerInner={chatAnswerContainerInner}
|
chatAnswerContainerInner={chatAnswerContainerInner}
|
||||||
hideProcessDetail={hideProcessDetail}
|
hideProcessDetail={hideProcessDetail}
|
||||||
|
noChatInput={noChatInput}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -95,6 +95,7 @@ export type IChatItem = {
|
|||||||
// for agent log
|
// for agent log
|
||||||
conversationId?: string
|
conversationId?: string
|
||||||
input?: any
|
input?: any
|
||||||
|
parentMessageId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Metadata = {
|
export type Metadata = {
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export const CONVERSATION_ID_INFO = 'conversationIdInfo'
|
export const CONVERSATION_ID_INFO = 'conversationIdInfo'
|
||||||
|
export const UUID_NIL = '00000000-0000-0000-0000-000000000000'
|
||||||
|
@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react'
|
|||||||
import Chat from '../chat'
|
import Chat from '../chat'
|
||||||
import type {
|
import type {
|
||||||
ChatConfig,
|
ChatConfig,
|
||||||
|
ChatItem,
|
||||||
OnSend,
|
OnSend,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { useChat } from '../chat/hooks'
|
import { useChat } from '../chat/hooks'
|
||||||
@ -45,11 +46,13 @@ const ChatWrapper = () => {
|
|||||||
} as ChatConfig
|
} as ChatConfig
|
||||||
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
||||||
const {
|
const {
|
||||||
|
chatListRef,
|
||||||
chatList,
|
chatList,
|
||||||
handleSend,
|
handleSend,
|
||||||
handleStop,
|
handleStop,
|
||||||
isResponding,
|
isResponding,
|
||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
|
handleUpdateChatList,
|
||||||
} = useChat(
|
} = useChat(
|
||||||
appConfig,
|
appConfig,
|
||||||
{
|
{
|
||||||
@ -65,11 +68,12 @@ const ChatWrapper = () => {
|
|||||||
currentChatInstanceRef.current.handleStop = handleStop
|
currentChatInstanceRef.current.handleStop = handleStop
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const doSend: OnSend = useCallback((message, files) => {
|
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||||
const data: any = {
|
const data: any = {
|
||||||
query: message,
|
query: message,
|
||||||
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
||||||
conversation_id: currentConversationId,
|
conversation_id: currentConversationId,
|
||||||
|
parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appConfig?.file_upload?.image.enabled && files?.length)
|
if (appConfig?.file_upload?.image.enabled && files?.length)
|
||||||
@ -85,6 +89,7 @@ const ChatWrapper = () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
|
chatListRef,
|
||||||
appConfig,
|
appConfig,
|
||||||
currentConversationId,
|
currentConversationId,
|
||||||
currentConversationItem,
|
currentConversationItem,
|
||||||
@ -94,6 +99,23 @@ const ChatWrapper = () => {
|
|||||||
isInstalledApp,
|
isInstalledApp,
|
||||||
appId,
|
appId,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||||
|
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||||
|
if (index === -1)
|
||||||
|
return
|
||||||
|
|
||||||
|
const prevMessages = chatList.slice(0, index)
|
||||||
|
const question = prevMessages.pop()
|
||||||
|
const lastAnswer = prevMessages.at(-1)
|
||||||
|
|
||||||
|
if (!question)
|
||||||
|
return
|
||||||
|
|
||||||
|
handleUpdateChatList(prevMessages)
|
||||||
|
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||||
|
}, [chatList, handleUpdateChatList, doSend])
|
||||||
|
|
||||||
const chatNode = useMemo(() => {
|
const chatNode = useMemo(() => {
|
||||||
if (inputsForms.length) {
|
if (inputsForms.length) {
|
||||||
return (
|
return (
|
||||||
@ -136,6 +158,7 @@ const ChatWrapper = () => {
|
|||||||
chatFooterClassName='pb-4'
|
chatFooterClassName='pb-4'
|
||||||
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
|
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
|
||||||
onSend={doSend}
|
onSend={doSend}
|
||||||
|
onRegenerate={doRegenerate}
|
||||||
onStopResponding={handleStop}
|
onStopResponding={handleStop}
|
||||||
chatNode={chatNode}
|
chatNode={chatNode}
|
||||||
allToolIcons={appMeta?.tool_icons || {}}
|
allToolIcons={appMeta?.tool_icons || {}}
|
||||||
|
@ -11,10 +11,10 @@ import { useLocalStorageState } from 'ahooks'
|
|||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import type {
|
import type {
|
||||||
ChatConfig,
|
ChatConfig,
|
||||||
ChatItem,
|
|
||||||
Feedback,
|
Feedback,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { CONVERSATION_ID_INFO } from '../constants'
|
import { CONVERSATION_ID_INFO } from '../constants'
|
||||||
|
import { getPrevChatList, getProcessedInputsFromUrlParams } from '../utils'
|
||||||
import {
|
import {
|
||||||
fetchAppInfo,
|
fetchAppInfo,
|
||||||
fetchAppMeta,
|
fetchAppMeta,
|
||||||
@ -28,10 +28,8 @@ import type {
|
|||||||
// AppData,
|
// AppData,
|
||||||
ConversationItem,
|
ConversationItem,
|
||||||
} from '@/models/share'
|
} from '@/models/share'
|
||||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
import { changeLanguage } from '@/i18n/i18next-config'
|
import { changeLanguage } from '@/i18n/i18next-config'
|
||||||
import { getProcessedInputsFromUrlParams } from '@/app/components/base/chat/utils'
|
|
||||||
|
|
||||||
export const useEmbeddedChatbot = () => {
|
export const useEmbeddedChatbot = () => {
|
||||||
const isInstalledApp = false
|
const isInstalledApp = false
|
||||||
@ -75,32 +73,12 @@ 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 appPrevChatList = useMemo(() => {
|
const appPrevChatList = useMemo(
|
||||||
const data = appChatListData?.data || []
|
() => (currentConversationId && appChatListData?.data.length)
|
||||||
const chatList: ChatItem[] = []
|
? getPrevChatList(appChatListData.data)
|
||||||
|
: [],
|
||||||
if (currentConversationId && data.length) {
|
[appChatListData, currentConversationId],
|
||||||
data.forEach((item: any) => {
|
)
|
||||||
chatList.push({
|
|
||||||
id: `question-${item.id}`,
|
|
||||||
content: item.query,
|
|
||||||
isAnswer: false,
|
|
||||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
|
||||||
})
|
|
||||||
chatList.push({
|
|
||||||
id: item.id,
|
|
||||||
content: item.answer,
|
|
||||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
|
||||||
feedback: item.feedback,
|
|
||||||
isAnswer: true,
|
|
||||||
citation: item.retriever_resources,
|
|
||||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return chatList
|
|
||||||
}, [appChatListData, currentConversationId])
|
|
||||||
|
|
||||||
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
|
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
|
||||||
|
|
||||||
@ -155,7 +133,7 @@ export const useEmbeddedChatbot = () => {
|
|||||||
type: 'text-input',
|
type: 'text-input',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [appParams])
|
}, [initInputs, appParams])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// init inputs from url params
|
// init inputs from url params
|
||||||
|
@ -63,7 +63,9 @@ export type ChatItem = IChatItem & {
|
|||||||
conversationId?: string
|
conversationId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OnSend = (message: string, files?: VisionFile[]) => void
|
export type OnSend = (message: string, files?: VisionFile[], last_answer?: ChatItem) => void
|
||||||
|
|
||||||
|
export type OnRegenerate = (chatItem: ChatItem) => void
|
||||||
|
|
||||||
export type Callback = {
|
export type Callback = {
|
||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
|
||||||
|
import { UUID_NIL } from './constants'
|
||||||
|
import type { ChatItem } from './types'
|
||||||
|
|
||||||
async function decodeBase64AndDecompress(base64String: string) {
|
async function decodeBase64AndDecompress(base64String: string) {
|
||||||
const binaryString = atob(base64String)
|
const binaryString = atob(base64String)
|
||||||
const compressedUint8Array = Uint8Array.from(binaryString, char => char.charCodeAt(0))
|
const compressedUint8Array = Uint8Array.from(binaryString, char => char.charCodeAt(0))
|
||||||
const decompressedStream = new Response(compressedUint8Array).body.pipeThrough(new DecompressionStream('gzip'))
|
const decompressedStream = new Response(compressedUint8Array).body?.pipeThrough(new DecompressionStream('gzip'))
|
||||||
const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer()
|
const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer()
|
||||||
return new TextDecoder().decode(decompressedArrayBuffer)
|
return new TextDecoder().decode(decompressedArrayBuffer)
|
||||||
}
|
}
|
||||||
@ -15,6 +19,57 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
|
|||||||
return inputs
|
return inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendQAToChatList(chatList: ChatItem[], item: any) {
|
||||||
|
// we append answer first and then question since will reverse the whole chatList later
|
||||||
|
chatList.push({
|
||||||
|
id: item.id,
|
||||||
|
content: item.answer,
|
||||||
|
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||||
|
feedback: item.feedback,
|
||||||
|
isAnswer: true,
|
||||||
|
citation: item.retriever_resources,
|
||||||
|
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||||
|
})
|
||||||
|
chatList.push({
|
||||||
|
id: `question-${item.id}`,
|
||||||
|
content: item.query,
|
||||||
|
isAnswer: false,
|
||||||
|
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the latest thread messages from all messages of the conversation.
|
||||||
|
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
|
||||||
|
*
|
||||||
|
* @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
|
||||||
|
* @returns An array of ChatItems representing the latest thread.
|
||||||
|
*/
|
||||||
|
function getPrevChatList(fetchedMessages: any[]) {
|
||||||
|
const ret: ChatItem[] = []
|
||||||
|
let nextMessageId = null
|
||||||
|
|
||||||
|
for (const item of fetchedMessages) {
|
||||||
|
if (!item.parent_message_id) {
|
||||||
|
appendQAToChatList(ret, item)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextMessageId) {
|
||||||
|
appendQAToChatList(ret, item)
|
||||||
|
nextMessageId = item.parent_message_id
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||||
|
appendQAToChatList(ret, item)
|
||||||
|
nextMessageId = item.parent_message_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getProcessedInputsFromUrlParams,
|
getProcessedInputsFromUrlParams,
|
||||||
|
getPrevChatList,
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>
|
After Width: | Height: | Size: 524 B |
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"icon": {
|
||||||
|
"type": "element",
|
||||||
|
"isRootNode": true,
|
||||||
|
"name": "svg",
|
||||||
|
"attributes": {
|
||||||
|
"xmlns": "http://www.w3.org/2000/svg",
|
||||||
|
"viewBox": "0 0 24 24",
|
||||||
|
"fill": "currentColor"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"d": "M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "Refresh"
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
// GENERATE BY script
|
||||||
|
// DON NOT EDIT IT MANUALLY
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import data from './Refresh.json'
|
||||||
|
import IconBase from '@/app/components/base/icons/IconBase'
|
||||||
|
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||||
|
|
||||||
|
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||||
|
props,
|
||||||
|
ref,
|
||||||
|
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||||
|
|
||||||
|
Icon.displayName = 'Refresh'
|
||||||
|
|
||||||
|
export default Icon
|
@ -18,6 +18,7 @@ export { default as Menu01 } from './Menu01'
|
|||||||
export { default as Pin01 } from './Pin01'
|
export { default as Pin01 } from './Pin01'
|
||||||
export { default as Pin02 } from './Pin02'
|
export { default as Pin02 } from './Pin02'
|
||||||
export { default as Plus02 } from './Plus02'
|
export { default as Plus02 } from './Plus02'
|
||||||
|
export { default as Refresh } from './Refresh'
|
||||||
export { default as Settings01 } from './Settings01'
|
export { default as Settings01 } from './Settings01'
|
||||||
export { default as Settings04 } from './Settings04'
|
export { default as Settings04 } from './Settings04'
|
||||||
export { default as Target04 } from './Target04'
|
export { default as Target04 } from './Target04'
|
||||||
|
31
web/app/components/base/regenerate-btn/index.tsx
Normal file
31
web/app/components/base/regenerate-btn/index.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
'use client'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
import { Refresh } from '../icons/src/vender/line/general'
|
||||||
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
onClick?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RegenerateBtn = ({ className, onClick }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className={`${className}`}>
|
||||||
|
<Tooltip
|
||||||
|
popupContent={t('appApi.regenerate') as string}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
|
||||||
|
onClick={() => onClick?.()}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Refresh className="p-[3.5px] w-6 h-6 text-[#667085] hover:bg-gray-50" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegenerateBtn
|
@ -2,7 +2,6 @@ import {
|
|||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
@ -17,50 +16,70 @@ import type { ChatItem } from '@/app/components/base/chat/types'
|
|||||||
import { fetchConversationMessages } from '@/service/debug'
|
import { fetchConversationMessages } from '@/service/debug'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import { UUID_NIL } from '@/app/components/base/chat/constants'
|
||||||
|
|
||||||
|
function appendQAToChatList(newChatList: ChatItem[], item: any) {
|
||||||
|
newChatList.push({
|
||||||
|
id: item.id,
|
||||||
|
content: item.answer,
|
||||||
|
feedback: item.feedback,
|
||||||
|
isAnswer: true,
|
||||||
|
citation: item.metadata?.retriever_resources,
|
||||||
|
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||||
|
workflow_run_id: item.workflow_run_id,
|
||||||
|
})
|
||||||
|
newChatList.push({
|
||||||
|
id: `question-${item.id}`,
|
||||||
|
content: item.query,
|
||||||
|
isAnswer: false,
|
||||||
|
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedChatList(messages: any[]) {
|
||||||
|
const newChatList: ChatItem[] = []
|
||||||
|
let nextMessageId = null
|
||||||
|
for (const item of messages) {
|
||||||
|
if (!item.parent_message_id) {
|
||||||
|
appendQAToChatList(newChatList, item)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextMessageId) {
|
||||||
|
appendQAToChatList(newChatList, item)
|
||||||
|
nextMessageId = item.parent_message_id
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||||
|
appendQAToChatList(newChatList, item)
|
||||||
|
nextMessageId = item.parent_message_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newChatList.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
const ChatRecord = () => {
|
const ChatRecord = () => {
|
||||||
const [fetched, setFetched] = useState(false)
|
const [fetched, setFetched] = useState(false)
|
||||||
const [chatList, setChatList] = useState([])
|
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||||
const appDetail = useAppStore(s => s.appDetail)
|
const appDetail = useAppStore(s => s.appDetail)
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const { handleLoadBackupDraft } = useWorkflowRun()
|
const { handleLoadBackupDraft } = useWorkflowRun()
|
||||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||||
const currentConversationID = historyWorkflowData?.conversation_id
|
const currentConversationID = historyWorkflowData?.conversation_id
|
||||||
|
|
||||||
const chatMessageList = useMemo(() => {
|
|
||||||
const res: ChatItem[] = []
|
|
||||||
if (chatList.length) {
|
|
||||||
chatList.forEach((item: any) => {
|
|
||||||
res.push({
|
|
||||||
id: `question-${item.id}`,
|
|
||||||
content: item.query,
|
|
||||||
isAnswer: false,
|
|
||||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
|
||||||
})
|
|
||||||
res.push({
|
|
||||||
id: item.id,
|
|
||||||
content: item.answer,
|
|
||||||
feedback: item.feedback,
|
|
||||||
isAnswer: true,
|
|
||||||
citation: item.metadata?.retriever_resources,
|
|
||||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
|
||||||
workflow_run_id: item.workflow_run_id,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}, [chatList])
|
|
||||||
|
|
||||||
const handleFetchConversationMessages = useCallback(async () => {
|
const handleFetchConversationMessages = useCallback(async () => {
|
||||||
if (appDetail && currentConversationID) {
|
if (appDetail && currentConversationID) {
|
||||||
try {
|
try {
|
||||||
setFetched(false)
|
setFetched(false)
|
||||||
const res = await fetchConversationMessages(appDetail.id, currentConversationID)
|
const res = await fetchConversationMessages(appDetail.id, currentConversationID)
|
||||||
setFetched(true)
|
setChatList(getFormattedChatList((res as any).data))
|
||||||
setChatList((res as any).data)
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setFetched(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [appDetail, currentConversationID])
|
}, [appDetail, currentConversationID])
|
||||||
@ -101,7 +120,7 @@ const ChatRecord = () => {
|
|||||||
config={{
|
config={{
|
||||||
supportCitationHitInfo: true,
|
supportCitationHitInfo: true,
|
||||||
} as any}
|
} as any}
|
||||||
chatList={chatMessageList}
|
chatList={chatList}
|
||||||
chatContainerClassName='px-4'
|
chatContainerClassName='px-4'
|
||||||
chatContainerInnerClassName='pt-6 w-full max-w-full mx-auto'
|
chatContainerInnerClassName='pt-6 w-full max-w-full mx-auto'
|
||||||
chatFooterClassName='px-4 rounded-b-2xl'
|
chatFooterClassName='px-4 rounded-b-2xl'
|
||||||
|
@ -18,7 +18,7 @@ import ConversationVariableModal from './conversation-variable-modal'
|
|||||||
import { useChat } from './hooks'
|
import { useChat } from './hooks'
|
||||||
import type { ChatWrapperRefType } from './index'
|
import type { ChatWrapperRefType } from './index'
|
||||||
import Chat from '@/app/components/base/chat/chat'
|
import Chat from '@/app/components/base/chat/chat'
|
||||||
import type { OnSend } from '@/app/components/base/chat/types'
|
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
|
||||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||||
import {
|
import {
|
||||||
fetchSuggestedQuestions,
|
fetchSuggestedQuestions,
|
||||||
@ -58,6 +58,8 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConv
|
|||||||
const {
|
const {
|
||||||
conversationId,
|
conversationId,
|
||||||
chatList,
|
chatList,
|
||||||
|
chatListRef,
|
||||||
|
handleUpdateChatList,
|
||||||
handleStop,
|
handleStop,
|
||||||
isResponding,
|
isResponding,
|
||||||
suggestedQuestions,
|
suggestedQuestions,
|
||||||
@ -73,19 +75,36 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConv
|
|||||||
taskId => stopChatMessageResponding(appDetail!.id, taskId),
|
taskId => stopChatMessageResponding(appDetail!.id, taskId),
|
||||||
)
|
)
|
||||||
|
|
||||||
const doSend = useCallback<OnSend>((query, files) => {
|
const doSend = useCallback<OnSend>((query, files, last_answer) => {
|
||||||
handleSend(
|
handleSend(
|
||||||
{
|
{
|
||||||
query,
|
query,
|
||||||
files,
|
files,
|
||||||
inputs: workflowStore.getState().inputs,
|
inputs: workflowStore.getState().inputs,
|
||||||
conversation_id: conversationId,
|
conversation_id: conversationId,
|
||||||
|
parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
|
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [conversationId, handleSend, workflowStore, appDetail])
|
}, [chatListRef, conversationId, handleSend, workflowStore, appDetail])
|
||||||
|
|
||||||
|
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||||
|
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||||
|
if (index === -1)
|
||||||
|
return
|
||||||
|
|
||||||
|
const prevMessages = chatList.slice(0, index)
|
||||||
|
const question = prevMessages.pop()
|
||||||
|
const lastAnswer = prevMessages.at(-1)
|
||||||
|
|
||||||
|
if (!question)
|
||||||
|
return
|
||||||
|
|
||||||
|
handleUpdateChatList(prevMessages)
|
||||||
|
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||||
|
}, [chatList, handleUpdateChatList, doSend])
|
||||||
|
|
||||||
useImperativeHandle(ref, () => {
|
useImperativeHandle(ref, () => {
|
||||||
return {
|
return {
|
||||||
@ -107,6 +126,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConv
|
|||||||
chatFooterClassName='px-4 rounded-bl-2xl'
|
chatFooterClassName='px-4 rounded-bl-2xl'
|
||||||
chatFooterInnerClassName='pb-4 w-full max-w-full mx-auto'
|
chatFooterInnerClassName='pb-4 w-full max-w-full mx-auto'
|
||||||
onSend={doSend}
|
onSend={doSend}
|
||||||
|
onRegenerate={doRegenerate}
|
||||||
onStopResponding={handleStop}
|
onStopResponding={handleStop}
|
||||||
chatNode={(
|
chatNode={(
|
||||||
<>
|
<>
|
||||||
|
@ -387,6 +387,8 @@ export const useChat = (
|
|||||||
return {
|
return {
|
||||||
conversationId: conversationId.current,
|
conversationId: conversationId.current,
|
||||||
chatList,
|
chatList,
|
||||||
|
chatListRef,
|
||||||
|
handleUpdateChatList,
|
||||||
handleSend,
|
handleSend,
|
||||||
handleStop,
|
handleStop,
|
||||||
handleRestart,
|
handleRestart,
|
||||||
|
@ -5,10 +5,10 @@ import type { AppIconType } from '@/types/app'
|
|||||||
|
|
||||||
type UseAppFaviconOptions = {
|
type UseAppFaviconOptions = {
|
||||||
enable?: boolean
|
enable?: boolean
|
||||||
icon_type?: AppIconType
|
icon_type?: AppIconType | null
|
||||||
icon?: string
|
icon?: string
|
||||||
icon_background?: string
|
icon_background?: string | null
|
||||||
icon_url?: string
|
icon_url?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAppFavicon(options: UseAppFaviconOptions) {
|
export function useAppFavicon(options: UseAppFaviconOptions) {
|
||||||
|
@ -6,6 +6,7 @@ const translation = {
|
|||||||
ok: 'In Service',
|
ok: 'In Service',
|
||||||
copy: 'Copy',
|
copy: 'Copy',
|
||||||
copied: 'Copied',
|
copied: 'Copied',
|
||||||
|
regenerate: 'Regenerate',
|
||||||
play: 'Play',
|
play: 'Play',
|
||||||
pause: 'Pause',
|
pause: 'Pause',
|
||||||
playing: 'Playing',
|
playing: 'Playing',
|
||||||
|
@ -6,6 +6,7 @@ const translation = {
|
|||||||
ok: '运行中',
|
ok: '运行中',
|
||||||
copy: '复制',
|
copy: '复制',
|
||||||
copied: '已复制',
|
copied: '已复制',
|
||||||
|
regenerate: '重新生成',
|
||||||
play: '播放',
|
play: '播放',
|
||||||
pause: '暂停',
|
pause: '暂停',
|
||||||
playing: '播放中',
|
playing: '播放中',
|
||||||
|
@ -106,6 +106,7 @@ export type MessageContent = {
|
|||||||
metadata: Metadata
|
metadata: Metadata
|
||||||
agent_thoughts: any[] // TODO
|
agent_thoughts: any[] // TODO
|
||||||
workflow_run_id: string
|
workflow_run_id: string
|
||||||
|
parent_message_id: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompletionConversationGeneralDetail = {
|
export type CompletionConversationGeneralDetail = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user