| HIDDEN_VALUE = "[__HIDDEN__]" | HIDDEN_VALUE = "[__HIDDEN__]" | ||||
| UUID_NIL = "00000000-0000-0000-0000-000000000000" |
| 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() |
| 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) | ||||
| 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: |
| 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() | ||||
| 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.") |
| 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"), |
| 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() |
| 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.") |
| 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 |
| 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, |
| 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, |
| 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, |
| 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", |
| """ | """ | ||||
| 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): |
| 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: |
| 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 |
| "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} |
| 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"), |
| """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 ### |
| 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) |
| 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) | ||||
| 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] |
| 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) => { |
| 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} />} |
| 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={{ |
| 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 || {}} |
| 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) | ||||
| 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} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| 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')} |
| | '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, |
| return { | return { | ||||
| chatList, | chatList, | ||||
| setChatList, | |||||
| chatListRef, | |||||
| handleUpdateChatList, | |||||
| conversationId: conversationId.current, | conversationId: conversationId.current, | ||||
| isResponding, | isResponding, | ||||
| setIsResponding, | setIsResponding, |
| 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} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| // for agent log | // for agent log | ||||
| conversationId?: string | conversationId?: string | ||||
| input?: any | input?: any | ||||
| parentMessageId?: string | |||||
| } | } | ||||
| export type Metadata = { | export type Metadata = { |
| export const CONVERSATION_ID_INFO = 'conversationIdInfo' | export const CONVERSATION_ID_INFO = 'conversationIdInfo' | ||||
| export const UUID_NIL = '00000000-0000-0000-0000-000000000000' |
| 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 || {}} |
| 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 |
| 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 |
| 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, | |||||
| } | } |
| <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> |
| { | |||||
| "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" | |||||
| } |
| // 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 |
| 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' |
| '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 |
| 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' |
| 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={( | ||||
| <> | <> |
| return { | return { | ||||
| conversationId: conversationId.current, | conversationId: conversationId.current, | ||||
| chatList, | chatList, | ||||
| chatListRef, | |||||
| handleUpdateChatList, | |||||
| handleSend, | handleSend, | ||||
| handleStop, | handleStop, | ||||
| handleRestart, | handleRestart, |
| 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) { |
| 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', |
| ok: '运行中', | ok: '运行中', | ||||
| copy: '复制', | copy: '复制', | ||||
| copied: '已复制', | copied: '已复制', | ||||
| regenerate: '重新生成', | |||||
| play: '播放', | play: '播放', | ||||
| pause: '暂停', | pause: '暂停', | ||||
| playing: '播放中', | playing: '播放中', |
| 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 = { |