浏览代码

feat: regenerate in `Chat`, `agent` and `Chatflow` app (#7661)

tags/0.9.0
Hash Brown 1年前
父节点
当前提交
8c51d06222
没有帐户链接到提交者的电子邮件
共有 51 个文件被更改,包括 604 次插入179 次删除
  1. 1
    0
      api/constants/__init__.py
  2. 1
    0
      api/controllers/console/app/completion.py
  3. 0
    2
      api/controllers/console/app/message.py
  4. 2
    0
      api/controllers/console/app/workflow.py
  5. 1
    0
      api/controllers/console/explore/completion.py
  6. 1
    1
      api/controllers/console/explore/message.py
  7. 1
    0
      api/controllers/service_api/app/message.py
  8. 1
    0
      api/controllers/web/completion.py
  9. 2
    1
      api/controllers/web/message.py
  10. 4
    1
      api/core/agent/base_agent_runner.py
  11. 1
    0
      api/core/app/apps/advanced_chat/app_generator.py
  12. 1
    0
      api/core/app/apps/agent_chat/app_generator.py
  13. 1
    0
      api/core/app/apps/chat/app_generator.py
  14. 1
    0
      api/core/app/apps/message_based_app_generator.py
  15. 3
    0
      api/core/app/entities/app_invoke_entities.py
  16. 18
    3
      api/core/memory/token_buffer_memory.py
  17. 22
    0
      api/core/prompt/utils/extract_thread_messages.py
  18. 1
    0
      api/fields/conversation_fields.py
  19. 1
    0
      api/fields/message_fields.py
  20. 36
    0
      api/migrations/versions/2024_09_11_1012-d57ba9ebb251_add_parent_message_id_to_messages.py
  21. 1
    0
      api/models/model.py
  22. 3
    1
      api/services/message_service.py
  23. 91
    0
      api/tests/unit_tests/core/prompt/test_extract_thread_messages.py
  24. 3
    1
      web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx
  25. 23
    3
      web/app/components/app/configuration/debug/debug-with-single-model/index.tsx
  26. 86
    63
      web/app/components/app/log/list.tsx
  27. 24
    1
      web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
  28. 7
    28
      web/app/components/base/chat/chat-with-history/hooks.tsx
  29. 3
    0
      web/app/components/base/chat/chat/answer/index.tsx
  30. 9
    4
      web/app/components/base/chat/chat/answer/operation.tsx
  31. 3
    0
      web/app/components/base/chat/chat/context.tsx
  32. 2
    1
      web/app/components/base/chat/chat/hooks.ts
  33. 5
    0
      web/app/components/base/chat/chat/index.tsx
  34. 1
    0
      web/app/components/base/chat/chat/type.ts
  35. 1
    0
      web/app/components/base/chat/constants.ts
  36. 24
    1
      web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
  37. 8
    30
      web/app/components/base/chat/embedded-chatbot/hooks.tsx
  38. 3
    1
      web/app/components/base/chat/types.ts
  39. 56
    1
      web/app/components/base/chat/utils.ts
  40. 1
    0
      web/app/components/base/icons/assets/vender/line/general/refresh.svg
  41. 23
    0
      web/app/components/base/icons/src/vender/line/general/Refresh.json
  42. 16
    0
      web/app/components/base/icons/src/vender/line/general/Refresh.tsx
  43. 1
    0
      web/app/components/base/icons/src/vender/line/general/index.ts
  44. 31
    0
      web/app/components/base/regenerate-btn/index.tsx
  45. 49
    30
      web/app/components/workflow/panel/chat-record/index.tsx
  46. 23
    3
      web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
  47. 2
    0
      web/app/components/workflow/panel/debug-and-preview/hooks.ts
  48. 3
    3
      web/hooks/use-app-favicon.ts
  49. 1
    0
      web/i18n/en-US/app-api.ts
  50. 1
    0
      web/i18n/zh-Hans/app-api.ts
  51. 1
    0
      web/models/log.ts

+ 1
- 0
api/constants/__init__.py 查看文件

HIDDEN_VALUE = "[__HIDDEN__]" HIDDEN_VALUE = "[__HIDDEN__]"
UUID_NIL = "00000000-0000-0000-0000-000000000000"

+ 1
- 0
api/controllers/console/app/completion.py 查看文件

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()

+ 0
- 2
api/controllers/console/app/message.py 查看文件

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)





+ 2
- 0
api/controllers/console/app/workflow.py 查看文件

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:

+ 1
- 0
api/controllers/console/explore/completion.py 查看文件

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()



+ 1
- 1
api/controllers/console/explore/message.py 查看文件



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.")

+ 1
- 0
api/controllers/service_api/app/message.py 查看文件

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"),

+ 1
- 0
api/controllers/web/completion.py 查看文件

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()

+ 2
- 1
api/controllers/web/message.py 查看文件

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"),


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.")

+ 4
- 1
api/core/agent/base_agent_runner.py 查看文件

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,
.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

+ 1
- 0
api/core/app/apps/advanced_chat/app_generator.py 查看文件

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,

+ 1
- 0
api/core/app/apps/agent_chat/app_generator.py 查看文件

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,

+ 1
- 0
api/core/app/apps/chat/app_generator.py 查看文件

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,

+ 1
- 0
api/core/app/apps/message_based_app_generator.py 查看文件

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",

+ 3
- 0
api/core/app/entities/app_invoke_entities.py 查看文件

""" """


conversation_id: Optional[str] = None conversation_id: Optional[str] = None
parent_message_id: Optional[str] = None




class CompletionAppGenerateEntity(EasyUIBasedAppGenerateEntity): class CompletionAppGenerateEntity(EasyUIBasedAppGenerateEntity):
""" """


conversation_id: Optional[str] = None conversation_id: Optional[str] = None
parent_message_id: Optional[str] = None




class AdvancedChatAppGenerateEntity(AppGenerateEntity): 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):

+ 18
- 3
api/core/memory/token_buffer_memory.py 查看文件

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


# 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)
.filter(Message.conversation_id == self.conversation.id, Message.answer != "")
db.session.query(
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())
) )




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
- 0
api/core/prompt/utils/extract_thread_messages.py 查看文件

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

+ 1
- 0
api/fields/conversation_fields.py 查看文件

"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}

+ 1
- 0
api/fields/message_fields.py 查看文件

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"),

+ 36
- 0
api/migrations/versions/2024_09_11_1012-d57ba9ebb251_add_parent_message_id_to_messages.py 查看文件

"""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 ###

+ 1
- 0
api/models/model.py 查看文件

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)

+ 3
- 1
api/services/message_service.py 查看文件

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)
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)



+ 91
- 0
api/tests/unit_tests/core/prompt/test_extract_thread_messages.py 查看文件

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]

+ 3
- 1
web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx 查看文件

const config = useConfigFromDebugContext() const config = useConfigFromDebugContext()
const { const {
chatList, chatList,
chatListRef,
isResponding, isResponding,
handleSend, handleSend,
suggestedQuestions, suggestedQuestions,
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)
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) => {

+ 23
- 3
web/app/components/app/configuration/debug/debug-with-single-model/index.tsx 查看文件

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,
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,
) )
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)
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)
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> = {}
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} />}

+ 86
- 63
web/app/components/app/log/list.tsx 查看文件

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'
frequency_penalty: 'Frequency Penalty', frequency_penalty: 'Frequency Penalty',
} }


// Format interface data for easy display
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
const newChatList: IChatItem[] = []
messages.forEach((item: ChatMessage) => {
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') || [],
})
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,
}
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,
}
if (item.annotation) {
return {
id: item.annotation.id,
authorName: item.annotation.account.name,
logAnnotation: item.annotation,
created_at: 0,
} }
}


return undefined
})(),
})
return undefined
})(),
parentMessageId: `question-${item.id}`,
}) })
return newChatList
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 newChatList: IChatItem[] = []
let nextMessageId = null
for (const item of messages) {
if (!item.parent_message_id) {
appendQAToChatList(newChatList, item, conversationId, timezone, format)
break
}

if (!nextMessageId) {
appendQAToChatList(newChatList, item, conversationId, timezone, format)
nextMessageId = item.parent_message_id
}
else {
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
appendQAToChatList(newChatList, item, conversationId, timezone, format)
nextMessageId = item.parent_message_id
}
}
}
return newChatList.reverse()
} }


// const displayedParams = CompletionParams.slice(0, -2) // const displayedParams = CompletionParams.slice(0, -2)
}))) })))
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 () => {
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',
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={{

+ 24
- 1
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx 查看文件

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'
}, [appParams, currentConversationItem?.introduction, currentConversationId]) }, [appParams, currentConversationItem?.introduction, currentConversationId])
const { const {
chatList, chatList,
chatListRef,
handleUpdateChatList,
handleSend, handleSend,
handleStop, handleStop,
isResponding, isResponding,
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)
}, },
) )
}, [ }, [
chatListRef,
appConfig, appConfig,
currentConversationId, currentConversationId,
currentConversationItem, currentConversationItem,
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 (
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 || {}}

+ 7
- 28
web/app/components/base/chat/chat-with-history/hooks.tsx 查看文件

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,
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'
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 data = appChatListData?.data || []
const chatList: ChatItem[] = []

if (currentConversationId && data.length) {
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 appPrevChatList = useMemo(
() => (currentConversationId && appChatListData?.data.length)
? getPrevChatList(appChatListData.data)
: [],
[appChatListData, currentConversationId],
)


const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false) const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)



+ 3
- 0
web/app/components/base/chat/chat/answer/index.tsx 查看文件

chatAnswerContainerInner?: string chatAnswerContainerInner?: string
hideProcessDetail?: boolean hideProcessDetail?: boolean
appData?: AppData appData?: AppData
noChatInput?: boolean
} }
const Answer: FC<AnswerProps> = ({ const Answer: FC<AnswerProps> = ({
item, item,
chatAnswerContainerInner, chatAnswerContainerInner,
hideProcessDetail, hideProcessDetail,
appData, appData,
noChatInput,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
question={question} question={question}
index={index} index={index}
showPromptLog={showPromptLog} showPromptLog={showPromptLog}
noChatInput={noChatInput}
/> />
) )
} }

+ 9
- 4
web/app/components/base/chat/chat/answer/operation.tsx 查看文件

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'
maxSize: number maxSize: number
contentWidth: number contentWidth: number
hasWorkflowProcess: boolean hasWorkflowProcess: boolean
noChatInput?: boolean
} }
const Operation: FC<OperationProps> = ({ const Operation: FC<OperationProps> = ({
item, item,
maxSize, maxSize,
contentWidth, contentWidth,
hasWorkflowProcess, hasWorkflowProcess,
noChatInput,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
onAnnotationEdited, onAnnotationEdited,
onAnnotationRemoved, onAnnotationRemoved,
onFeedback, onFeedback,
onRegenerate,
} = useChatContext() } = useChatContext()
const [isShowReplyModal, setIsShowReplyModal] = useState(false) const [isShowReplyModal, setIsShowReplyModal] = useState(false)
const { const {
</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'>
<Tooltip
popupContent={t('appDebug.operation.agree')}
>
<div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
<Tooltip popupContent={t('appDebug.operation.agree')}>
<div <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')}

+ 3
- 0
web/app/components/base/chat/chat/context.tsx 查看文件

| 'answerIcon' | 'answerIcon'
| 'allToolIcons' | 'allToolIcons'
| 'onSend' | 'onSend'
| 'onRegenerate'
| 'onAnnotationEdited' | 'onAnnotationEdited'
| 'onAnnotationAdded' | 'onAnnotationAdded'
| 'onAnnotationRemoved' | 'onAnnotationRemoved'
answerIcon, answerIcon,
allToolIcons, allToolIcons,
onSend, onSend,
onRegenerate,
onAnnotationEdited, onAnnotationEdited,
onAnnotationAdded, onAnnotationAdded,
onAnnotationRemoved, onAnnotationRemoved,
answerIcon, answerIcon,
allToolIcons, allToolIcons,
onSend, onSend,
onRegenerate,
onAnnotationEdited, onAnnotationEdited,
onAnnotationAdded, onAnnotationAdded,
onAnnotationRemoved, onAnnotationRemoved,

+ 2
- 1
web/app/components/base/chat/chat/hooks.ts 查看文件



return { return {
chatList, chatList,
setChatList,
chatListRef,
handleUpdateChatList,
conversationId: conversationId.current, conversationId: conversationId.current,
isResponding, isResponding,
setIsResponding, setIsResponding,

+ 5
- 0
web/app/components/base/chat/chat/index.tsx 查看文件

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'
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
appData, appData,
config, config,
onSend, onSend,
onRegenerate,
chatList, chatList,
isResponding, isResponding,
noStopResponding, noStopResponding,
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}
showPromptLog={showPromptLog} showPromptLog={showPromptLog}
chatAnswerContainerInner={chatAnswerContainerInner} chatAnswerContainerInner={chatAnswerContainerInner}
hideProcessDetail={hideProcessDetail} hideProcessDetail={hideProcessDetail}
noChatInput={noChatInput}
/> />
) )
} }

+ 1
- 0
web/app/components/base/chat/chat/type.ts 查看文件

// for agent log // for agent log
conversationId?: string conversationId?: string
input?: any input?: any
parentMessageId?: string
} }


export type Metadata = { export type Metadata = {

+ 1
- 0
web/app/components/base/chat/constants.ts 查看文件

export const CONVERSATION_ID_INFO = 'conversationIdInfo' export const CONVERSATION_ID_INFO = 'conversationIdInfo'
export const UUID_NIL = '00000000-0000-0000-0000-000000000000'

+ 24
- 1
web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx 查看文件

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'
} 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,
{ {
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)
}, },
) )
}, [ }, [
chatListRef,
appConfig, appConfig,
currentConversationId, currentConversationId,
currentConversationItem, currentConversationItem,
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 (
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 || {}}

+ 8
- 30
web/app/components/base/chat/embedded-chatbot/hooks.tsx 查看文件

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,
// 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
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 data = appChatListData?.data || []
const chatList: ChatItem[] = []

if (currentConversationId && data.length) {
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 appPrevChatList = useMemo(
() => (currentConversationId && appChatListData?.data.length)
? getPrevChatList(appChatListData.data)
: [],
[appChatListData, currentConversationId],
)


const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false) const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)


type: 'text-input', type: 'text-input',
} }
}) })
}, [appParams])
}, [initInputs, appParams])


useEffect(() => { useEffect(() => {
// init inputs from url params // init inputs from url params

+ 3
- 1
web/app/components/base/chat/types.ts 查看文件

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

+ 56
- 1
web/app/components/base/chat/utils.ts 查看文件

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)
} }
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,
} }

+ 1
- 0
web/app/components/base/icons/assets/vender/line/general/refresh.svg 查看文件

<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>

+ 23
- 0
web/app/components/base/icons/src/vender/line/general/Refresh.json 查看文件

{
"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"
}

+ 16
- 0
web/app/components/base/icons/src/vender/line/general/Refresh.tsx 查看文件

// 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

+ 1
- 0
web/app/components/base/icons/src/vender/line/general/index.ts 查看文件

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
- 0
web/app/components/base/regenerate-btn/index.tsx 查看文件

'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

+ 49
- 30
web/app/components/workflow/panel/chat-record/index.tsx 查看文件

memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useState, useState,
} from 'react' } from 'react'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
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((res as any).data)
setChatList(getFormattedChatList((res as any).data))
} }
catch (e) { catch (e) {

console.error(e)
}
finally {
setFetched(true)
} }
} }
}, [appDetail, currentConversationID]) }, [appDetail, currentConversationID])
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'

+ 23
- 3
web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx 查看文件

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,
const { const {
conversationId, conversationId,
chatList, chatList,
chatListRef,
handleUpdateChatList,
handleStop, handleStop,
isResponding, isResponding,
suggestedQuestions, suggestedQuestions,
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 {
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={(
<> <>

+ 2
- 0
web/app/components/workflow/panel/debug-and-preview/hooks.ts 查看文件

return { return {
conversationId: conversationId.current, conversationId: conversationId.current,
chatList, chatList,
chatListRef,
handleUpdateChatList,
handleSend, handleSend,
handleStop, handleStop,
handleRestart, handleRestart,

+ 3
- 3
web/hooks/use-app-favicon.ts 查看文件



type UseAppFaviconOptions = { type UseAppFaviconOptions = {
enable?: boolean enable?: boolean
icon_type?: AppIconType
icon_type?: AppIconType | null
icon?: string icon?: string
icon_background?: string
icon_url?: string
icon_background?: string | null
icon_url?: string | null
} }


export function useAppFavicon(options: UseAppFaviconOptions) { export function useAppFavicon(options: UseAppFaviconOptions) {

+ 1
- 0
web/i18n/en-US/app-api.ts 查看文件

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',

+ 1
- 0
web/i18n/zh-Hans/app-api.ts 查看文件

ok: '运行中', ok: '运行中',
copy: '复制', copy: '复制',
copied: '已复制', copied: '已复制',
regenerate: '重新生成',
play: '播放', play: '播放',
pause: '暂停', pause: '暂停',
playing: '播放中', playing: '播放中',

+ 1
- 0
web/models/log.ts 查看文件

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 = {

正在加载...
取消
保存