mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: 工作流表单节点
This commit is contained in:
parent
4e615db713
commit
1a1e93296e
|
|
@ -7,6 +7,7 @@
|
|||
@desc:
|
||||
"""
|
||||
import time
|
||||
import uuid
|
||||
from abc import abstractmethod
|
||||
from typing import Type, Dict, List
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
|
|||
if workflow.is_result(node, NodeResult(step_variable, global_variable)) and 'answer' in step_variable:
|
||||
answer = step_variable['answer']
|
||||
yield answer
|
||||
workflow.answer += answer
|
||||
workflow.append_answer(answer)
|
||||
if global_variable is not None:
|
||||
for key in global_variable:
|
||||
workflow.context[key] = global_variable[key]
|
||||
|
|
@ -54,15 +55,27 @@ class WorkFlowPostHandler:
|
|||
'message_tokens' in row and row.get('message_tokens') is not None])
|
||||
answer_tokens = sum([row.get('answer_tokens') for row in details.values() if
|
||||
'answer_tokens' in row and row.get('answer_tokens') is not None])
|
||||
chat_record = ChatRecord(id=chat_record_id,
|
||||
chat_id=chat_id,
|
||||
problem_text=question,
|
||||
answer_text=answer,
|
||||
details=details,
|
||||
message_tokens=message_tokens,
|
||||
answer_tokens=answer_tokens,
|
||||
run_time=time.time() - workflow.context['start_time'],
|
||||
index=0)
|
||||
answer_text_list = workflow.get_answer_text_list()
|
||||
answer_text = '\n\n'.join(answer_text_list)
|
||||
if workflow.chat_record is not None:
|
||||
chat_record = workflow.chat_record
|
||||
chat_record.answer_text = answer_text
|
||||
chat_record.details = details
|
||||
chat_record.message_tokens = message_tokens
|
||||
chat_record.answer_tokens = answer_tokens
|
||||
chat_record.answer_text_list = answer_text_list
|
||||
chat_record.run_time = time.time() - workflow.context['start_time']
|
||||
else:
|
||||
chat_record = ChatRecord(id=chat_record_id,
|
||||
chat_id=chat_id,
|
||||
problem_text=question,
|
||||
answer_text=answer_text,
|
||||
details=details,
|
||||
message_tokens=message_tokens,
|
||||
answer_tokens=answer_tokens,
|
||||
answer_text_list=answer_text_list,
|
||||
run_time=time.time() - workflow.context['start_time'],
|
||||
index=0)
|
||||
self.chat_info.append_chat_record(chat_record, self.client_id)
|
||||
# 重新设置缓存
|
||||
chat_cache.set(chat_id,
|
||||
|
|
@ -118,7 +131,15 @@ class FlowParamsSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class INode:
|
||||
def __init__(self, node, workflow_params, workflow_manage):
|
||||
|
||||
@abstractmethod
|
||||
def save_context(self, details, workflow_manage):
|
||||
pass
|
||||
|
||||
def get_answer_text(self):
|
||||
return self.answer_text
|
||||
|
||||
def __init__(self, node, workflow_params, workflow_manage, runtime_node_id=None):
|
||||
# 当前步骤上下文,用于存储当前步骤信息
|
||||
self.status = 200
|
||||
self.err_message = ''
|
||||
|
|
@ -129,7 +150,12 @@ class INode:
|
|||
self.node_params_serializer = None
|
||||
self.flow_params_serializer = None
|
||||
self.context = {}
|
||||
self.answer_text = None
|
||||
self.id = node.id
|
||||
if runtime_node_id is None:
|
||||
self.runtime_node_id = str(uuid.uuid1())
|
||||
else:
|
||||
self.runtime_node_id = runtime_node_id
|
||||
|
||||
def valid_args(self, node_params, flow_params):
|
||||
flow_params_serializer_class = self.get_flow_params_serializer_class()
|
||||
|
|
|
|||
|
|
@ -9,19 +9,23 @@
|
|||
from .ai_chat_step_node import *
|
||||
from .application_node import BaseApplicationNode
|
||||
from .condition_node import *
|
||||
from .question_node import *
|
||||
from .search_dataset_node import *
|
||||
from .start_node import *
|
||||
from .direct_reply_node import *
|
||||
from .form_node import *
|
||||
from .function_lib_node import *
|
||||
from .function_node import *
|
||||
from .question_node import *
|
||||
from .reranker_node import *
|
||||
|
||||
from .document_extract_node import *
|
||||
from .image_understand_step_node import *
|
||||
|
||||
from .search_dataset_node import *
|
||||
from .start_node import *
|
||||
|
||||
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode,
|
||||
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode, BaseDocumentExtractNode,
|
||||
BaseImageUnderstandNode]
|
||||
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
|
||||
BaseDocumentExtractNode,
|
||||
BaseImageUnderstandNode, BaseFormNode]
|
||||
|
||||
|
||||
def get_node(node_type):
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wo
|
|||
node.context['question'] = node_variable['question']
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
|
||||
workflow.answer += answer
|
||||
node.answer_text = answer
|
||||
|
||||
|
||||
def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
|
|
@ -73,6 +73,11 @@ def get_default_model_params_setting(model_id):
|
|||
|
||||
|
||||
class BaseChatNode(IChatNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
|
||||
model_params_setting=None,
|
||||
**kwargs) -> NodeResult:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wo
|
|||
node.context['question'] = node_variable['question']
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
|
||||
workflow.answer += answer
|
||||
node.answer_text = answer
|
||||
|
||||
|
||||
def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
|
|
@ -64,6 +64,12 @@ def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wor
|
|||
|
||||
class BaseApplicationNode(IApplicationNode):
|
||||
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['type'] = details.get('type')
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, application_id, message, chat_id, chat_record_id, stream, re_chat, client_id, client_type,
|
||||
**kwargs) -> NodeResult:
|
||||
from application.serializers.chat_message_serializers import ChatMessageSerializer
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ from application.flow.step_node.condition_node.i_condition_node import IConditio
|
|||
|
||||
|
||||
class BaseConditionNode(IConditionNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['branch_id'] = details.get('branch_id')
|
||||
self.context['branch_name'] = details.get('branch_name')
|
||||
|
||||
def execute(self, **kwargs) -> NodeResult:
|
||||
branch_list = self.node_params_serializer.data['branch']
|
||||
branch = self._execute(branch_list)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ from application.flow.step_node.direct_reply_node.i_reply_node import IReplyNode
|
|||
|
||||
|
||||
class BaseReplyNode(IReplyNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.answer_text = details.get('answer')
|
||||
def execute(self, reply_type, stream, fields=None, content=None, **kwargs) -> NodeResult:
|
||||
if reply_type == 'referencing':
|
||||
result = self.get_reference_content(fields)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/11/4 14:48
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: i_form_node.py
|
||||
@date:2024/11/4 14:48
|
||||
@desc:
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.util.field_message import ErrMessage
|
||||
|
||||
|
||||
class FormNodeParamsSerializer(serializers.Serializer):
|
||||
form_field_list = serializers.ListField(required=True, error_messages=ErrMessage.list("表单配置"))
|
||||
form_content_format = serializers.CharField(required=True, error_messages=ErrMessage.char('表单输出内容'))
|
||||
|
||||
|
||||
class IFormNode(INode):
|
||||
type = 'form-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return FormNodeParamsSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, form_field_list, form_content_format, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/11/4 14:49
|
||||
@desc:
|
||||
"""
|
||||
from .base_form_node import BaseFormNode
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: base_form_node.py
|
||||
@date:2024/11/4 14:52
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.form_node.i_form_node import IFormNode
|
||||
|
||||
|
||||
def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
|
||||
if step_variable is not None:
|
||||
for key in step_variable:
|
||||
node.context[key] = step_variable[key]
|
||||
if workflow.is_result(node, NodeResult(step_variable, global_variable)) and 'result' in step_variable:
|
||||
result = step_variable['result']
|
||||
yield result
|
||||
node.answer_text = result
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
|
||||
class BaseFormNode(IFormNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
self.context['form_content_format'] = details.get('form_content_format')
|
||||
self.context['form_field_list'] = details.get('form_field_list')
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.context['start_time'] = details.get('start_time')
|
||||
self.answer_text = details.get('result')
|
||||
|
||||
def execute(self, form_field_list, form_content_format, **kwargs) -> NodeResult:
|
||||
form_setting = {"form_field_list": form_field_list, "runtime_node_id": self.runtime_node_id,
|
||||
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
|
||||
"is_submit": self.context.get("is_submit", False)}
|
||||
form = f'<form_rander>{json.dumps(form_setting)}</form_rander>'
|
||||
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
|
||||
value = prompt_template.format(form=form)
|
||||
return NodeResult(
|
||||
{'result': value, 'form_field_list': form_field_list, 'form_content_format': form_content_format}, {},
|
||||
_write_context=write_context)
|
||||
|
||||
def get_answer_text(self):
|
||||
form_content_format = self.context.get('form_content_format')
|
||||
form_field_list = self.context.get('form_field_list')
|
||||
form_setting = {"form_field_list": form_field_list, "runtime_node_id": self.runtime_node_id,
|
||||
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
|
||||
'form_data': self.context.get('form_data', {}),
|
||||
"is_submit": self.context.get("is_submit", False)}
|
||||
form = f'<form_rander>{json.dumps(form_setting)}</form_rander>'
|
||||
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
|
||||
value = prompt_template.format(form=form)
|
||||
return value
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
form_content_format = self.context.get('form_content_format')
|
||||
form_field_list = self.context.get('form_field_list')
|
||||
form_setting = {"form_field_list": form_field_list, "runtime_node_id": self.runtime_node_id,
|
||||
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
|
||||
'form_data': self.context.get('form_data', {}),
|
||||
"is_submit": self.context.get("is_submit", False)}
|
||||
form = f'<form_rander>{json.dumps(form_setting)}</form_rander>'
|
||||
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
|
||||
value = prompt_template.format(form=form)
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
"result": value,
|
||||
"form_content_format": self.context.get('form_content_format'),
|
||||
"form_field_list": self.context.get('form_field_list'),
|
||||
'form_data': self.context.get('form_data'),
|
||||
'start_time': self.context.get('start_time'),
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -91,6 +91,9 @@ def convert_value(name: str, value, _type, is_required, source, node):
|
|||
|
||||
|
||||
class BaseFunctionLibNodeNode(IFunctionLibNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
self.answer_text = details.get('result')
|
||||
def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult:
|
||||
function_lib = QuerySet(FunctionLib).filter(id=function_lib_id).first()
|
||||
if not function_lib.is_active:
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ def convert_value(name: str, value, _type, is_required, source, node):
|
|||
|
||||
|
||||
class BaseFunctionNodeNode(IFunctionNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
self.answer_text = details.get('result')
|
||||
|
||||
def execute(self, input_field_list, code, **kwargs) -> NodeResult:
|
||||
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
|
||||
field.get('is_required'), field.get('source'), self)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wor
|
|||
|
||||
|
||||
class BaseImageUnderstandNode(IImageUnderstandNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['question'] = details.get('question')
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
|
||||
image,
|
||||
**kwargs) -> NodeResult:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wo
|
|||
node.context['question'] = node_variable['question']
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
|
||||
workflow.answer += answer
|
||||
node.answer_text = answer
|
||||
|
||||
|
||||
def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
|
|
@ -73,6 +73,14 @@ def get_default_model_params_setting(model_id):
|
|||
|
||||
|
||||
class BaseQuestionNode(IQuestionNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['answer'] = details.get('answer')
|
||||
self.context['message_tokens'] = details.get('message_tokens')
|
||||
self.context['answer_tokens'] = details.get('answer_tokens')
|
||||
self.answer_text = details.get('answer')
|
||||
|
||||
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
|
||||
model_params_setting=None,
|
||||
**kwargs) -> NodeResult:
|
||||
|
|
|
|||
|
|
@ -44,6 +44,13 @@ def filter_result(document_list: List[Document], max_paragraph_char_number, top_
|
|||
|
||||
|
||||
class BaseRerankerNode(IRerankerNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['document_list'] = details.get('document_list', [])
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.context['result_list'] = details.get('result_list')
|
||||
self.context['result'] = details.get('result')
|
||||
|
||||
def execute(self, question, reranker_setting, reranker_list, reranker_model_id,
|
||||
**kwargs) -> NodeResult:
|
||||
documents = merge_reranker_list(reranker_list)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,21 @@ def reset_title(title):
|
|||
|
||||
|
||||
class BaseSearchDatasetNode(ISearchDatasetStepNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
result = details.get('paragraph_list', [])
|
||||
dataset_setting = self.node_params_serializer.data.get('dataset_setting')
|
||||
directly_return = '\n'.join(
|
||||
[f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in result if
|
||||
paragraph.get('is_hit_handling_method')])
|
||||
self.context['paragraph_list'] = result
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.context['is_hit_handling_method_list'] = [row for row in result if row.get('is_hit_handling_method')]
|
||||
self.context['data'] = '\n'.join(
|
||||
[f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in
|
||||
result])[0:dataset_setting.get('max_paragraph_char_number', 5000)]
|
||||
self.context['directly_return'] = directly_return
|
||||
|
||||
def execute(self, dataset_id_list, dataset_setting, question,
|
||||
exclude_paragraph_id_list=None,
|
||||
**kwargs) -> NodeResult:
|
||||
|
|
|
|||
|
|
@ -6,9 +6,6 @@
|
|||
@date:2024/6/3 16:54
|
||||
@desc:
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,17 @@ def get_global_variable(node):
|
|||
|
||||
|
||||
class BaseStartStepNode(IStarNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
base_node = self.workflow_manage.get_base_node()
|
||||
default_global_variable = get_default_global_variable(base_node.properties.get('input_field_list', []))
|
||||
workflow_variable = {**default_global_variable, **get_global_variable(self)}
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
self.status = details.get('status')
|
||||
self.err_message = details.get('err_message')
|
||||
for key, value in workflow_variable.items():
|
||||
workflow_manage.context[key] = value
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
import json
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import reduce
|
||||
from typing import List, Dict
|
||||
|
|
@ -212,12 +211,12 @@ class NodeChunkManage:
|
|||
except IndexError as e:
|
||||
if self.current_node_chunk.is_end():
|
||||
self.current_node_chunk = None
|
||||
if len(self.work_flow.answer) > 0:
|
||||
if self.work_flow.answer_is_not_empty():
|
||||
chunk = self.work_flow.base_to_response.to_stream_chunk_response(
|
||||
self.work_flow.params['chat_id'],
|
||||
self.work_flow.params['chat_record_id'],
|
||||
'\n\n', False, 0, 0)
|
||||
self.work_flow.answer += '\n\n'
|
||||
self.work_flow.append_answer('\n\n')
|
||||
return chunk
|
||||
return self.pop()
|
||||
return None
|
||||
|
|
@ -240,29 +239,65 @@ class NodeChunk:
|
|||
|
||||
class WorkflowManage:
|
||||
def __init__(self, flow: Flow, params, work_flow_post_handler: WorkFlowPostHandler,
|
||||
base_to_response: BaseToResponse = SystemToResponse(), form_data=None, image_list=None):
|
||||
base_to_response: BaseToResponse = SystemToResponse(), form_data=None, image_list=None,
|
||||
start_node_id=None,
|
||||
start_node_data=None, chat_record=None):
|
||||
if form_data is None:
|
||||
form_data = {}
|
||||
if image_list is None:
|
||||
image_list = []
|
||||
self.start_node = None
|
||||
self.start_node_result_future = None
|
||||
self.form_data = form_data
|
||||
self.image_list = image_list
|
||||
self.params = params
|
||||
self.flow = flow
|
||||
self.lock = threading.Lock()
|
||||
self.context = {}
|
||||
self.node_context = []
|
||||
self.node_chunk_manage = NodeChunkManage(self)
|
||||
self.work_flow_post_handler = work_flow_post_handler
|
||||
self.current_node = None
|
||||
self.current_result = None
|
||||
self.answer = ""
|
||||
self.answer_list = ['']
|
||||
self.status = 0
|
||||
self.base_to_response = base_to_response
|
||||
self.chat_record = chat_record
|
||||
if start_node_id is not None:
|
||||
self.load_node(chat_record, start_node_id, start_node_data)
|
||||
else:
|
||||
self.node_context = []
|
||||
|
||||
def append_answer(self, content):
|
||||
self.answer += content
|
||||
self.answer_list[-1] += content
|
||||
|
||||
def answer_is_not_empty(self):
|
||||
return len(self.answer_list[-1]) > 0
|
||||
|
||||
def load_node(self, chat_record, start_node_id, start_node_data):
|
||||
self.node_context = []
|
||||
self.answer = chat_record.answer_text
|
||||
self.answer_list = chat_record.answer_text_list
|
||||
self.answer_list.append('')
|
||||
for node_details in sorted(chat_record.details.values(), key=lambda d: d.get('index')):
|
||||
node_id = node_details.get('node_id')
|
||||
if node_details.get('runtime_node_id') == start_node_id:
|
||||
self.start_node = self.get_node_cls_by_id(node_id, node_details.get('runtime_node_id'))
|
||||
self.start_node.valid_args(self.start_node.node_params, self.start_node.workflow_params)
|
||||
self.start_node.save_context(node_details, self)
|
||||
node_result = NodeResult({**start_node_data, 'form_data': start_node_data, 'is_submit': True}, {})
|
||||
self.start_node_result_future = NodeResultFuture(node_result, None)
|
||||
return
|
||||
node_id = node_details.get('node_id')
|
||||
node = self.get_node_cls_by_id(node_id, node_details.get('runtime_node_id'))
|
||||
node.valid_args(node.node_params, node.workflow_params)
|
||||
node.save_context(node_details, self)
|
||||
self.node_context.append(node)
|
||||
|
||||
def run(self):
|
||||
if self.params.get('stream'):
|
||||
return self.run_stream()
|
||||
return self.run_stream(self.start_node, self.start_node_result_future)
|
||||
return self.run_block()
|
||||
|
||||
def run_block(self):
|
||||
|
|
@ -270,7 +305,7 @@ class WorkflowManage:
|
|||
非流式响应
|
||||
@return: 结果
|
||||
"""
|
||||
result = self.run_chain_async(None)
|
||||
result = self.run_chain_async(None, None)
|
||||
result.result()
|
||||
details = self.get_runtime_details()
|
||||
message_tokens = sum([row.get('message_tokens') for row in details.values() if
|
||||
|
|
@ -285,12 +320,12 @@ class WorkflowManage:
|
|||
, message_tokens, answer_tokens,
|
||||
_status=status.HTTP_200_OK if self.status == 200 else status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def run_stream(self):
|
||||
def run_stream(self, current_node, node_result_future):
|
||||
"""
|
||||
流式响应
|
||||
@return:
|
||||
"""
|
||||
result = self.run_chain_async(None)
|
||||
result = self.run_chain_async(current_node, node_result_future)
|
||||
return tools.to_stream_response_simple(self.await_result(result))
|
||||
|
||||
def await_result(self, result):
|
||||
|
|
@ -307,21 +342,23 @@ class WorkflowManage:
|
|||
if chunk is None:
|
||||
break
|
||||
yield chunk
|
||||
yield self.get_chunk_content('', True)
|
||||
finally:
|
||||
self.work_flow_post_handler.handler(self.params['chat_id'], self.params['chat_record_id'],
|
||||
self.answer,
|
||||
self)
|
||||
yield self.get_chunk_content('', True)
|
||||
|
||||
def run_chain_async(self, current_node):
|
||||
future = executor.submit(self.run_chain, current_node)
|
||||
def run_chain_async(self, current_node, node_result_future):
|
||||
future = executor.submit(self.run_chain, current_node, node_result_future)
|
||||
return future
|
||||
|
||||
def run_chain(self, current_node):
|
||||
def run_chain(self, current_node, node_result_future=None):
|
||||
if current_node is None:
|
||||
start_node = self.get_start_node()
|
||||
current_node = get_node(start_node.type)(start_node, self.params, self)
|
||||
node_result_future = self.run_node_future(current_node)
|
||||
if node_result_future is None:
|
||||
node_result_future = self.run_node_future(current_node)
|
||||
try:
|
||||
is_stream = self.params.get('stream', True)
|
||||
# 处理节点响应
|
||||
|
|
@ -335,7 +372,7 @@ class WorkflowManage:
|
|||
# 获取到可执行的子节点
|
||||
result_list = []
|
||||
for node in node_list:
|
||||
result = self.run_chain_async(node)
|
||||
result = self.run_chain_async(node, None)
|
||||
result_list.append(result)
|
||||
[r.result() for r in result_list]
|
||||
if self.status == 0:
|
||||
|
|
@ -445,10 +482,41 @@ class WorkflowManage:
|
|||
details_result = {}
|
||||
for index in range(len(self.node_context)):
|
||||
node = self.node_context[index]
|
||||
if self.chat_record is not None and self.chat_record.details is not None:
|
||||
details = self.chat_record.details.get(node.runtime_node_id)
|
||||
if details is not None and self.start_node.runtime_node_id != node.runtime_node_id:
|
||||
details_result[node.runtime_node_id] = details
|
||||
continue
|
||||
details = node.get_details(index)
|
||||
details_result[str(uuid.uuid1())] = details
|
||||
details['node_id'] = node.id
|
||||
details['runtime_node_id'] = node.runtime_node_id
|
||||
details_result[node.runtime_node_id] = details
|
||||
return details_result
|
||||
|
||||
def get_answer_text_list(self):
|
||||
answer_text_list = []
|
||||
for index in range(len(self.node_context)):
|
||||
node = self.node_context[index]
|
||||
answer_text = node.get_answer_text()
|
||||
if answer_text is not None:
|
||||
if self.chat_record is not None and self.chat_record.details is not None:
|
||||
details = self.chat_record.details.get(node.runtime_node_id)
|
||||
if details is not None and self.start_node.runtime_node_id != node.runtime_node_id:
|
||||
continue
|
||||
answer_text_list.append(
|
||||
{'content': answer_text, 'type': 'form' if node.type == 'form-node' else 'md'})
|
||||
result = []
|
||||
for index in range(len(answer_text_list)):
|
||||
answer = answer_text_list[index]
|
||||
if index == 0:
|
||||
result.append(answer.get('content'))
|
||||
continue
|
||||
if answer.get('type') != answer_text_list[index - 1]:
|
||||
result.append(answer.get('content'))
|
||||
else:
|
||||
result[-1] += answer.get('content')
|
||||
return result
|
||||
|
||||
def get_next_node(self):
|
||||
"""
|
||||
获取下一个可运行的所有节点
|
||||
|
|
@ -485,6 +553,8 @@ class WorkflowManage:
|
|||
@param current_node_result: 当前可执行节点结果
|
||||
@return: 可执行节点列表
|
||||
"""
|
||||
if current_node.type == 'form-node' and 'form_data' not in current_node_result.node_variable:
|
||||
return []
|
||||
node_list = []
|
||||
if current_node_result is not None and current_node_result.is_assertion_result():
|
||||
for edge in self.flow.edges:
|
||||
|
|
@ -537,7 +607,6 @@ class WorkflowManage:
|
|||
prompt = prompt.replace(globeLabel, globeValue)
|
||||
context[node.id] = node.context
|
||||
prompt_template = PromptTemplate.from_template(prompt, template_format='jinja2')
|
||||
|
||||
value = prompt_template.format(context=context)
|
||||
return value
|
||||
|
||||
|
|
@ -557,11 +626,11 @@ class WorkflowManage:
|
|||
base_node_list = [node for node in self.flow.nodes if node.type == 'base-node']
|
||||
return base_node_list[0]
|
||||
|
||||
def get_node_cls_by_id(self, node_id):
|
||||
def get_node_cls_by_id(self, node_id, runtime_node_id=None):
|
||||
for node in self.flow.nodes:
|
||||
if node.id == node_id:
|
||||
node_instance = get_node(node.type)(node,
|
||||
self.params, self)
|
||||
self.params, self, runtime_node_id)
|
||||
return node_instance
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
# Generated by Django 4.2.15 on 2024-11-07 11:22
|
||||
# Generated by Django 4.2.15 on 2024-11-13 10:13
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
sql = """
|
||||
UPDATE "public".application_chat_record
|
||||
SET "answer_text_list" = ARRAY[answer_text];
|
||||
"""
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('application', '0018_workflowversion_name'),
|
||||
]
|
||||
|
|
@ -20,4 +25,11 @@ class Migration(migrations.Migration):
|
|||
name='file_upload_setting',
|
||||
field=models.JSONField(default={}, verbose_name='文件上传相关设置'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chatrecord',
|
||||
name='answer_text_list',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=40960), default=list,
|
||||
size=None, verbose_name='改进标注列表'),
|
||||
),
|
||||
migrations.RunSQL(sql)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -147,6 +147,9 @@ class ChatRecord(AppModelMixin):
|
|||
default=VoteChoices.UN_VOTE)
|
||||
problem_text = models.CharField(max_length=10240, verbose_name="问题")
|
||||
answer_text = models.CharField(max_length=40960, verbose_name="答案")
|
||||
answer_text_list = ArrayField(verbose_name="改进标注列表",
|
||||
base_field=models.CharField(max_length=40960)
|
||||
, default=list)
|
||||
message_tokens = models.IntegerField(verbose_name="请求token数量", default=0)
|
||||
answer_tokens = models.IntegerField(verbose_name="响应token数量", default=0)
|
||||
const = models.IntegerField(verbose_name="总费用", default=0)
|
||||
|
|
|
|||
|
|
@ -214,10 +214,15 @@ class OpenAIChatSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class ChatMessageSerializer(serializers.Serializer):
|
||||
chat_id = serializers.UUIDField(required=True, error_messages=ErrMessage.char("对话id"))
|
||||
chat_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("对话id"))
|
||||
message = serializers.CharField(required=True, error_messages=ErrMessage.char("用户问题"))
|
||||
stream = serializers.BooleanField(required=True, error_messages=ErrMessage.char("是否流式回答"))
|
||||
re_chat = serializers.BooleanField(required=True, error_messages=ErrMessage.char("是否重新回答"))
|
||||
chat_record_id = serializers.UUIDField(required=False, allow_null=True,
|
||||
error_messages=ErrMessage.uuid("对话记录id"))
|
||||
runtime_node_id = serializers.CharField(required=False, allow_null=True, allow_blank=True,
|
||||
error_messages=ErrMessage.char("节点id"))
|
||||
node_data = serializers.DictField(required=False, error_messages=ErrMessage.char("节点参数"))
|
||||
application_id = serializers.UUIDField(required=False, allow_null=True, error_messages=ErrMessage.uuid("应用id"))
|
||||
client_id = serializers.CharField(required=True, error_messages=ErrMessage.char("客户端id"))
|
||||
client_type = serializers.CharField(required=True, error_messages=ErrMessage.char("客户端类型"))
|
||||
|
|
@ -293,6 +298,19 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||
pipeline_message.run(params)
|
||||
return pipeline_message.context['chat_result']
|
||||
|
||||
@staticmethod
|
||||
def get_chat_record(chat_info, chat_record_id):
|
||||
if chat_info is not None:
|
||||
chat_record_list = [chat_record for chat_record in chat_info.chat_record_list if
|
||||
str(chat_record.id) == str(chat_record_id)]
|
||||
if chat_record_list is not None and len(chat_record_list):
|
||||
return chat_record_list[-1]
|
||||
chat_record = QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_info.chat_id).first()
|
||||
if chat_record is None:
|
||||
raise ChatException(500, "对话纪要不存在")
|
||||
chat_record = QuerySet(ChatRecord).filter(id=chat_record_id).first()
|
||||
return chat_record
|
||||
|
||||
def chat_work_flow(self, chat_info: ChatInfo, base_to_response):
|
||||
message = self.data.get('message')
|
||||
re_chat = self.data.get('re_chat')
|
||||
|
|
@ -302,15 +320,21 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||
form_data = self.data.get('form_data')
|
||||
image_list = self.data.get('image_list')
|
||||
user_id = chat_info.application.user_id
|
||||
chat_record_id = self.data.get('chat_record_id')
|
||||
chat_record = None
|
||||
if chat_record_id is not None:
|
||||
chat_record = self.get_chat_record(chat_info, chat_record_id)
|
||||
work_flow_manage = WorkflowManage(Flow.new_instance(chat_info.work_flow_version.work_flow),
|
||||
{'history_chat_record': chat_info.chat_record_list, 'question': message,
|
||||
'chat_id': chat_info.chat_id, 'chat_record_id': str(uuid.uuid1()),
|
||||
'chat_id': chat_info.chat_id, 'chat_record_id': str(
|
||||
uuid.uuid1()) if chat_record is None else chat_record.id,
|
||||
'stream': stream,
|
||||
're_chat': re_chat,
|
||||
'client_id': client_id,
|
||||
'client_type': client_type,
|
||||
'user_id': user_id}, WorkFlowPostHandler(chat_info, client_id, client_type),
|
||||
base_to_response, form_data, image_list)
|
||||
base_to_response, form_data, image_list, self.data.get('runtime_node_id'),
|
||||
self.data.get('node_data'), chat_record)
|
||||
r = work_flow_manage.run()
|
||||
return r
|
||||
|
||||
|
|
|
|||
|
|
@ -397,7 +397,7 @@ class ChatRecordSerializerModel(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = ChatRecord
|
||||
fields = ['id', 'chat_id', 'vote_status', 'problem_text', 'answer_text',
|
||||
'message_tokens', 'answer_tokens', 'const', 'improve_paragraph_id_list', 'run_time', 'index',
|
||||
'message_tokens', 'answer_tokens', 'const', 'improve_paragraph_id_list', 'run_time', 'index','answer_text_list',
|
||||
'create_time', 'update_time']
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -129,9 +129,14 @@ class ChatView(APIView):
|
|||
'client_id': request.auth.client_id,
|
||||
'form_data': (request.data.get(
|
||||
'form_data') if 'form_data' in request.data else {}),
|
||||
|
||||
'image_list': request.data.get(
|
||||
'image_list') if 'image_list' in request.data else [],
|
||||
'client_type': request.auth.client_type}).chat()
|
||||
'client_type': request.auth.client_type,
|
||||
'runtime_node_id': request.data.get('runtime_node_id', None),
|
||||
'node_data': request.data.get('node_data', {}),
|
||||
'chat_record_id': request.data.get('chat_record_id')}
|
||||
).chat()
|
||||
|
||||
@action(methods=['GET'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="获取对话列表",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='file',
|
||||
name='meta',
|
||||
field=models.JSONField(default={}, verbose_name='文件关联数据'),
|
||||
field=models.JSONField(default=dict, verbose_name='文件关联数据'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ declare module 'markdown-it-sub'
|
|||
declare module 'markdown-it-sup'
|
||||
declare module 'markdown-it-toc-done-right'
|
||||
declare module 'katex'
|
||||
interface Window {
|
||||
sendMessage: ?((message: string, other_params_data: any) => void)
|
||||
}
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
declare type Recordable<T = any> = Record<string, T>;
|
||||
declare type Recordable<T = any> = Record<string, T>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ interface chatType {
|
|||
problem_text: string
|
||||
answer_text: string
|
||||
buffer: Array<String>
|
||||
answer_text_list: Array<string>
|
||||
/**
|
||||
* 是否写入结束
|
||||
*/
|
||||
|
|
@ -36,6 +37,7 @@ interface chatType {
|
|||
*/
|
||||
is_stop?: boolean
|
||||
record_id: string
|
||||
chat_id: string
|
||||
vote_status: string
|
||||
status?: number
|
||||
}
|
||||
|
|
@ -56,18 +58,25 @@ export class ChatRecordManage {
|
|||
this.is_close = false
|
||||
this.write_ed = false
|
||||
}
|
||||
append_answer(chunk_answer: String) {
|
||||
this.chat.answer_text_list[this.chat.answer_text_list.length - 1] =
|
||||
this.chat.answer_text_list[this.chat.answer_text_list.length - 1] + chunk_answer
|
||||
this.chat.answer_text = this.chat.answer_text + chunk_answer
|
||||
}
|
||||
write() {
|
||||
this.chat.is_stop = false
|
||||
this.is_stop = false
|
||||
this.is_close = false
|
||||
this.write_ed = false
|
||||
this.chat.write_ed = false
|
||||
if (this.loading) {
|
||||
this.loading.value = true
|
||||
}
|
||||
this.id = setInterval(() => {
|
||||
if (this.chat.buffer.length > 20) {
|
||||
this.chat.answer_text =
|
||||
this.chat.answer_text + this.chat.buffer.splice(0, this.chat.buffer.length - 20).join('')
|
||||
this.append_answer(this.chat.buffer.splice(0, this.chat.buffer.length - 20).join(''))
|
||||
} else if (this.is_close) {
|
||||
this.chat.answer_text = this.chat.answer_text + this.chat.buffer.splice(0).join('')
|
||||
this.append_answer(this.chat.buffer.splice(0).join(''))
|
||||
this.chat.write_ed = true
|
||||
this.write_ed = true
|
||||
if (this.loading) {
|
||||
|
|
@ -79,7 +88,7 @@ export class ChatRecordManage {
|
|||
} else {
|
||||
const s = this.chat.buffer.shift()
|
||||
if (s !== undefined) {
|
||||
this.chat.answer_text = this.chat.answer_text + s
|
||||
this.append_answer(s)
|
||||
}
|
||||
}
|
||||
}, this.ms)
|
||||
|
|
@ -95,6 +104,10 @@ export class ChatRecordManage {
|
|||
close() {
|
||||
this.is_close = true
|
||||
}
|
||||
open() {
|
||||
this.is_close = false
|
||||
this.is_stop = false
|
||||
}
|
||||
append(answer_text_block: string) {
|
||||
for (let index = 0; index < answer_text_block.length; index++) {
|
||||
this.chat.buffer.push(answer_text_block[index])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.05704 15.3334H2.00004C1.82323 15.3334 1.65366 15.2632 1.52864 15.1382C1.40361 15.0131 1.33337 14.8436 1.33337 14.6667V1.33341C1.33337 1.1566 1.40361 0.987035 1.52864 0.862011C1.65366 0.736987 1.82323 0.666748 2.00004 0.666748H12.6667C12.8435 0.666748 13.0131 0.736987 13.1381 0.862011C13.2631 0.987035 13.3334 1.1566 13.3334 1.33341V6.05641C13.3334 6.23316 13.2633 6.4027 13.1384 6.52775L4.52871 15.1381C4.46678 15.2 4.39324 15.2492 4.31231 15.2827C4.23138 15.3162 4.14464 15.3334 4.05704 15.3334ZM4.431 7.90245C4.49352 7.96496 4.5783 8.00008 4.66671 8.00008H8.00004C8.08844 8.00008 8.17323 7.96496 8.23574 7.90245C8.29825 7.83994 8.33337 7.75515 8.33337 7.66675V7.00008C8.33337 6.91167 8.29825 6.82689 8.23574 6.76438C8.17323 6.70187 8.08844 6.66675 8.00004 6.66675H4.66671C4.5783 6.66675 4.49352 6.70187 4.431 6.76438C4.36849 6.82689 4.33337 6.91167 4.33337 7.00008V7.66675C4.33337 7.75515 4.36849 7.83994 4.431 7.90245ZM4.431 4.56912C4.49352 4.63163 4.5783 4.66675 4.66671 4.66675H10.3334C10.3771 4.66675 10.4205 4.65813 10.4609 4.64138C10.5014 4.62462 10.5381 4.60007 10.5691 4.56912C10.6 4.53816 10.6246 4.50142 10.6413 4.46098C10.6581 4.42053 10.6667 4.37719 10.6667 4.33341V3.66675C10.6667 3.62297 10.6581 3.57963 10.6413 3.53919C10.6246 3.49874 10.6 3.462 10.5691 3.43105C10.5381 3.40009 10.5014 3.37554 10.4609 3.35879C10.4205 3.34204 10.3771 3.33341 10.3334 3.33341H4.66671C4.5783 3.33341 4.49352 3.36853 4.431 3.43105C4.36849 3.49356 4.33337 3.57834 4.33337 3.66675V4.33341C4.33337 4.42182 4.36849 4.5066 4.431 4.56912Z" fill="white"/>
|
||||
<g opacity="0.5">
|
||||
<path d="M13.565 11.518L11.6847 9.6381L7.55305 13.7961L7.33337 15.5777C7.33337 15.6661 7.36849 15.7509 7.431 15.8134C7.49352 15.8759 7.5783 15.911 7.66671 15.911L9.45064 15.627L13.565 11.518Z" fill="white"/>
|
||||
<path d="M14.0486 8.20917C13.7886 7.94884 13.387 7.92884 13.151 8.16417L12.1543 9.16417L14.0369 11.0468L15.0346 10.0508L15.0662 10.0168C15.2689 9.7781 15.2396 9.4001 14.9912 9.15144L14.0486 8.20917Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<div class="item-content mb-16 lighter">
|
||||
<div v-for="(answer_text, index) in chatRecord.answer_text_list">
|
||||
<div class="avatar">
|
||||
<img v-if="application.avatar" :src="application.avatar" height="30px" />
|
||||
<LogoIcon v-else height="30px" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-card shadow="always" class="dialog-card">
|
||||
<MdRenderer
|
||||
v-if="answer_text"
|
||||
:source="answer_text"
|
||||
:send-message="chatMessage"
|
||||
></MdRenderer>
|
||||
<span v-else-if="chatRecord.is_stop" shadow="always" class="dialog-card">
|
||||
已停止回答
|
||||
</span>
|
||||
<span v-else shadow="always" class="dialog-card">
|
||||
回答中 <span class="dotting"></span>
|
||||
</span>
|
||||
<!-- 知识来源 -->
|
||||
<div v-if="showSource(chatRecord) && index === chatRecord.answer_text_list.length - 1">
|
||||
<KnowledgeSource :data="chatRecord" :type="application.type" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<OperationButton
|
||||
:type="type"
|
||||
:application="application"
|
||||
:chat-record="chatRecord"
|
||||
:loading="loading"
|
||||
:start-chat="startChat"
|
||||
:stop-chat="stopChat"
|
||||
:regenerationChart="regenerationChart"
|
||||
></OperationButton>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import KnowledgeSource from '@/components/ai-chat/KnowledgeSource.vue'
|
||||
import MdRenderer from '@/components/markdown/MdRenderer.vue'
|
||||
import OperationButton from '@/components/ai-chat/component/operation-button/index.vue'
|
||||
import { type chatType } from '@/api/type/application'
|
||||
const props = defineProps<{
|
||||
chatRecord: chatType
|
||||
application: any
|
||||
loading: boolean
|
||||
sendMessage: (question: string, other_params_data?: any, chat?: chatType) => void
|
||||
chatManagement: any
|
||||
type: 'log' | 'ai-chat' | 'debug-ai-chat'
|
||||
}>()
|
||||
|
||||
const chatMessage = (question: string, type: 'old' | 'new', other_params_data?: any) => {
|
||||
if (type === 'old') {
|
||||
props.chatRecord.answer_text_list.push('')
|
||||
props.sendMessage(question, other_params_data, props.chatRecord)
|
||||
props.chatManagement.write(props.chatRecord.id)
|
||||
} else {
|
||||
props.sendMessage(question, other_params_data)
|
||||
}
|
||||
}
|
||||
|
||||
function showSource(row: any) {
|
||||
if (props.type === 'log') {
|
||||
return true
|
||||
} else if (row.write_ed && 500 !== row.status) {
|
||||
if (props.type === 'debug-ai-chat' || props.application?.show_source) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
const regenerationChart = (question: string) => {
|
||||
props.sendMessage(question, { rechat: true })
|
||||
}
|
||||
const stopChat = (chat: chatType) => {
|
||||
props.chatManagement.stop(chat.id)
|
||||
}
|
||||
const startChat = (chat: chatType) => {
|
||||
props.chatManagement.write(chat.id)
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
float: left;
|
||||
}
|
||||
.content {
|
||||
padding-left: var(--padding-left);
|
||||
|
||||
:deep(ol) {
|
||||
margin-left: 16px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
<template>
|
||||
<div></div>
|
||||
<div class="ai-chat__operate p-16-24">
|
||||
<slot name="operateBefore" />
|
||||
<div class="operate-textarea flex chat-width">
|
||||
<el-input
|
||||
ref="quickInputRef"
|
||||
v-model="inputValue"
|
||||
:placeholder="
|
||||
startRecorderTime
|
||||
? '说话中...'
|
||||
: recorderLoading
|
||||
? '转文字中...'
|
||||
: '请输入问题,Ctrl+Enter 换行,Enter发送'
|
||||
"
|
||||
:autosize="{ minRows: 1, maxRows: isMobile ? 4 : 10 }"
|
||||
type="textarea"
|
||||
:maxlength="100000"
|
||||
@keydown.enter="sendChatHandle($event)"
|
||||
/>
|
||||
|
||||
<div class="operate flex align-center">
|
||||
<span v-if="props.applicationDetails.file_upload_enable" class="flex align-center">
|
||||
<el-upload
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="(file: any, fileList: any) => uploadFile(file, fileList)"
|
||||
>
|
||||
<el-button text>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-divider direction="vertical" />
|
||||
</span>
|
||||
<span v-if="props.applicationDetails.stt_model_enable" class="flex align-center">
|
||||
<el-button text v-if="mediaRecorderStatus" @click="startRecording">
|
||||
<el-icon>
|
||||
<Microphone />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<div v-else class="operate flex align-center">
|
||||
<el-text type="info"
|
||||
>00:{{ recorderTime < 10 ? `0${recorderTime}` : recorderTime }}</el-text
|
||||
>
|
||||
<el-button text type="primary" @click="stopRecording" :loading="recorderLoading">
|
||||
<AppIcon iconName="app-video-stop"></AppIcon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-divider v-if="!startRecorderTime && !recorderLoading" direction="vertical" />
|
||||
</span>
|
||||
|
||||
<el-button
|
||||
v-if="!startRecorderTime && !recorderLoading"
|
||||
text
|
||||
class="sent-button"
|
||||
:disabled="isDisabledChart || loading"
|
||||
@click="sendChatHandle"
|
||||
>
|
||||
<img v-show="isDisabledChart || loading" src="@/assets/icon_send.svg" alt="" />
|
||||
<SendIcon v-show="!isDisabledChart && !loading" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="chat-width text-center"
|
||||
v-if="applicationDetails.disclaimer"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
<el-text type="info" v-if="applicationDetails.disclaimer" style="font-size: 12px">
|
||||
<auto-tooltip :content="applicationDetails.disclaimer_value">
|
||||
{{ applicationDetails.disclaimer_value }}
|
||||
</auto-tooltip>
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import Recorder from 'recorder-core'
|
||||
import applicationApi from '@/api/application'
|
||||
import { MsgAlert } from '@/utils/message'
|
||||
import { type chatType } from '@/api/type/application'
|
||||
import { useRoute } from 'vue-router'
|
||||
import 'recorder-core/src/engine/mp3'
|
||||
import 'recorder-core/src/engine/mp3-engine'
|
||||
import { MsgWarning } from '@/utils/message'
|
||||
const route = useRoute()
|
||||
const {
|
||||
query: { mode }
|
||||
} = route as any
|
||||
const quickInputRef = ref()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
applicationDetails: any
|
||||
type: 'log' | 'ai-chat' | 'debug-ai-chat'
|
||||
loading: boolean
|
||||
isMobile: boolean
|
||||
appId?: string
|
||||
chatId: string
|
||||
sendMessage: (question: string, other_params_data?: any, chat?: chatType) => void
|
||||
}>(),
|
||||
{
|
||||
applicationDetails: () => ({}),
|
||||
available: true
|
||||
}
|
||||
)
|
||||
const emit = defineEmits(['update:chatId', 'update:loading'])
|
||||
const chartOpenId = ref<string>()
|
||||
const chatId_context = computed({
|
||||
get: () => {
|
||||
if (chartOpenId.value) {
|
||||
return chartOpenId.value
|
||||
}
|
||||
return props.chatId
|
||||
},
|
||||
set: (v) => {
|
||||
chartOpenId.value = v
|
||||
emit('update:chatId', v)
|
||||
}
|
||||
})
|
||||
const localLoading = computed({
|
||||
get: () => {
|
||||
return props.loading
|
||||
},
|
||||
set: (v) => {
|
||||
emit('update:loading', v)
|
||||
}
|
||||
})
|
||||
const uploadFile = async (file: any, fileList: any) => {
|
||||
const { maxFiles, fileLimit } = props.applicationDetails.file_upload_setting
|
||||
if (fileList.length > maxFiles) {
|
||||
MsgWarning('最多上传' + maxFiles + '个文件')
|
||||
return
|
||||
}
|
||||
if (fileList.filter((f: any) => f.size > fileLimit * 1024 * 1024).length > 0) {
|
||||
// MB
|
||||
MsgWarning('单个文件大小不能超过' + fileLimit + 'MB')
|
||||
fileList.splice(0, fileList.length)
|
||||
return
|
||||
}
|
||||
const formData = new FormData()
|
||||
for (const file of fileList) {
|
||||
formData.append('file', file.raw, file.name)
|
||||
uploadFileList.value.push(file)
|
||||
}
|
||||
|
||||
if (!chatId_context.value) {
|
||||
const res = await applicationApi.getChatOpen(props.applicationDetails.id as string)
|
||||
chatId_context.value = res.data
|
||||
}
|
||||
|
||||
applicationApi
|
||||
.uploadFile(
|
||||
props.applicationDetails.id as string,
|
||||
chatId_context.value as string,
|
||||
formData,
|
||||
localLoading
|
||||
)
|
||||
.then((response) => {
|
||||
fileList.splice(0, fileList.length)
|
||||
uploadFileList.value.forEach((file: any) => {
|
||||
const f = response.data.filter((f: any) => f.name === file.name)
|
||||
if (f.length > 0) {
|
||||
file.url = f[0].url
|
||||
file.file_id = f[0].file_id
|
||||
}
|
||||
})
|
||||
console.log(uploadFileList.value)
|
||||
})
|
||||
}
|
||||
const recorderTime = ref(0)
|
||||
const startRecorderTime = ref(false)
|
||||
const recorderLoading = ref(false)
|
||||
const inputValue = ref<string>('')
|
||||
const uploadFileList = ref<Array<any>>([])
|
||||
const mediaRecorderStatus = ref(true)
|
||||
// 定义响应式引用
|
||||
const mediaRecorder = ref<any>(null)
|
||||
const isDisabledChart = computed(
|
||||
() => !(inputValue.value.trim() && (props.appId || props.applicationDetails?.name))
|
||||
)
|
||||
|
||||
// 开始录音
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
// 取消录音控制台日志
|
||||
Recorder.CLog = function () {}
|
||||
mediaRecorderStatus.value = false
|
||||
handleTimeChange()
|
||||
mediaRecorder.value = new Recorder({
|
||||
type: 'mp3',
|
||||
bitRate: 128,
|
||||
sampleRate: 16000
|
||||
})
|
||||
|
||||
mediaRecorder.value.open(
|
||||
() => {
|
||||
mediaRecorder.value.start()
|
||||
},
|
||||
(err: any) => {
|
||||
MsgAlert(
|
||||
`提示`,
|
||||
`<p>该功能需要使用麦克风,浏览器禁止不安全页面录音,解决方案如下:<br/>
|
||||
1、可开启 https 解决;<br/>
|
||||
2、若无 https 配置则需要修改浏览器安全配置,Chrome 设置如下:<br/>
|
||||
(1) 地址栏输入chrome://flags/#unsafely-treat-insecure-origin-as-secure;<br/>
|
||||
(2) 将 http 站点配置在文本框中,例如: http://127.0.0.1:8080。</p>
|
||||
<img src="${new URL(`../../assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
|
||||
{
|
||||
confirmButtonText: '我知道了',
|
||||
dangerouslyUseHTMLString: true,
|
||||
customClass: 'record-tip-confirm'
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
MsgAlert(
|
||||
`提示`,
|
||||
`<p>该功能需要使用麦克风,浏览器禁止不安全页面录音,解决方案如下:<br/>
|
||||
1、可开启 https 解决;<br/>
|
||||
2、若无 https 配置则需要修改浏览器安全配置,Chrome 设置如下:<br/>
|
||||
(1) 地址栏输入chrome://flags/#unsafely-treat-insecure-origin-as-secure;<br/>
|
||||
(2) 将 http 站点配置在文本框中,例如: http://127.0.0.1:8080。</p>
|
||||
<img src="${new URL(`../../assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
|
||||
{
|
||||
confirmButtonText: '我知道了',
|
||||
dangerouslyUseHTMLString: true,
|
||||
customClass: 'record-tip-confirm'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 停止录音
|
||||
const stopRecording = () => {
|
||||
startRecorderTime.value = false
|
||||
recorderTime.value = 0
|
||||
if (mediaRecorder.value) {
|
||||
mediaRecorderStatus.value = true
|
||||
mediaRecorder.value.stop(
|
||||
(blob: Blob, duration: number) => {
|
||||
// 测试blob是否能正常播放
|
||||
// const link = document.createElement('a')
|
||||
// link.href = window.URL.createObjectURL(blob)
|
||||
// link.download = 'abc.mp3'
|
||||
// link.click()
|
||||
uploadRecording(blob) // 上传录音文件
|
||||
},
|
||||
(err: any) => {
|
||||
console.error('录音失败:', err)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 上传录音文件
|
||||
const uploadRecording = async (audioBlob: Blob) => {
|
||||
try {
|
||||
recorderLoading.value = true
|
||||
const formData = new FormData()
|
||||
formData.append('file', audioBlob, 'recording.mp3')
|
||||
applicationApi
|
||||
.postSpeechToText(props.applicationDetails.id as string, formData, localLoading)
|
||||
.then((response) => {
|
||||
recorderLoading.value = false
|
||||
mediaRecorder.value.close()
|
||||
inputValue.value = typeof response.data === 'string' ? response.data : ''
|
||||
// chatMessage(null, res.data)
|
||||
})
|
||||
} catch (error) {
|
||||
recorderLoading.value = false
|
||||
console.error('上传失败:', error)
|
||||
}
|
||||
}
|
||||
const handleTimeChange = () => {
|
||||
startRecorderTime.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
if (recorderTime.value === 60) {
|
||||
recorderTime.value = 0
|
||||
stopRecording()
|
||||
startRecorderTime.value = false
|
||||
}
|
||||
if (!startRecorderTime.value) {
|
||||
return
|
||||
}
|
||||
recorderTime.value++
|
||||
handleTimeChange()
|
||||
}, 1000)
|
||||
}
|
||||
function sendChatHandle(event: any) {
|
||||
if (!event.ctrlKey) {
|
||||
// 如果没有按下组合键ctrl,则会阻止默认事件
|
||||
event.preventDefault()
|
||||
if (!isDisabledChart.value && !props.loading && !event.isComposing) {
|
||||
if (inputValue.value.trim()) {
|
||||
props.sendMessage(inputValue.value, { image_list: uploadFileList.value })
|
||||
inputValue.value = ''
|
||||
uploadFileList.value = []
|
||||
quickInputRef.value.textareaStyle.height = '45px'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果同时按下ctrl+回车键,则会换行
|
||||
inputValue.value += '\n'
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
if (quickInputRef.value && mode === 'embed') {
|
||||
quickInputRef.value.textarea.style.height = '0'
|
||||
}
|
||||
}, 1800)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.ai-chat {
|
||||
&__operate {
|
||||
background: #f3f7f9;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
z-index: 10;
|
||||
|
||||
&:before {
|
||||
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: -16px;
|
||||
left: 0;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.operate-textarea {
|
||||
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ffffff;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:has(.el-textarea__inner:focus) {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-textarea__inner {
|
||||
border-radius: 8px !important;
|
||||
box-shadow: none;
|
||||
resize: none;
|
||||
padding: 12px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.operate {
|
||||
padding: 6px 10px;
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sent-button {
|
||||
max-height: none;
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-loading-spinner {
|
||||
margin-top: -15px;
|
||||
|
||||
.circular {
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-width {
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.chat-width {
|
||||
max-width: 100% !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
</span>
|
||||
<span v-if="applicationId">
|
||||
<span v-if="applicationId && type == 'log'">
|
||||
<el-tooltip effect="dark" content="换个答案" placement="top">
|
||||
<el-button :disabled="chat_loading" text @click="regeneration">
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
|
|
@ -90,26 +90,21 @@ const {
|
|||
params: { id }
|
||||
} = route as any
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
applicationId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
chatId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
chat_loading: {
|
||||
type: Boolean
|
||||
},
|
||||
log: Boolean,
|
||||
tts: Boolean,
|
||||
tts_type: String
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: any
|
||||
type: 'log' | 'ai-chat' | 'debug-ai-chat'
|
||||
chatId: string
|
||||
chat_loading: boolean
|
||||
applicationId: string
|
||||
tts: boolean
|
||||
tts_type: string
|
||||
}>(),
|
||||
{
|
||||
data: () => ({}),
|
||||
type: 'ai-chat'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits(['update:data', 'regeneration'])
|
||||
|
||||
|
|
@ -1,59 +1,61 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-text type="info">
|
||||
<span class="ml-4">{{ datetimeFormat(data.create_time) }}</span>
|
||||
</el-text>
|
||||
</div>
|
||||
<div>
|
||||
<!-- 语音播放 -->
|
||||
<span v-if="tts">
|
||||
<el-tooltip effect="dark" content="点击播放" placement="top" v-if="!audioPlayerStatus">
|
||||
<el-button text @click="playAnswerText(data?.answer_text)">
|
||||
<AppIcon iconName="app-video-play"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else effect="dark" content="停止" placement="top">
|
||||
<el-button type="primary" text @click="pausePlayAnswerText()">
|
||||
<AppIcon iconName="app-video-pause"></AppIcon>
|
||||
<div class="flex-between mt-8">
|
||||
<div>
|
||||
<el-text type="info">
|
||||
<span class="ml-4">{{ datetimeFormat(data.create_time) }}</span>
|
||||
</el-text>
|
||||
</div>
|
||||
<div>
|
||||
<!-- 语音播放 -->
|
||||
<span v-if="tts">
|
||||
<el-tooltip effect="dark" content="点击播放" placement="top" v-if="!audioPlayerStatus">
|
||||
<el-button text @click="playAnswerText(data?.answer_text)">
|
||||
<AppIcon iconName="app-video-play"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else effect="dark" content="停止" placement="top">
|
||||
<el-button type="primary" text @click="pausePlayAnswerText()">
|
||||
<AppIcon iconName="app-video-pause"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
</span>
|
||||
<el-tooltip effect="dark" content="复制" placement="top">
|
||||
<el-button text @click="copyClick(data?.answer_text)">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
</span>
|
||||
<el-tooltip effect="dark" content="复制" placement="top">
|
||||
<el-button text @click="copyClick(data?.answer_text)">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
<el-tooltip
|
||||
v-if="data.improve_paragraph_id_list.length === 0"
|
||||
effect="dark"
|
||||
content="修改内容"
|
||||
placement="top"
|
||||
>
|
||||
<el-button text @click="editContent(data)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="data.improve_paragraph_id_list.length === 0"
|
||||
effect="dark"
|
||||
content="修改内容"
|
||||
placement="top"
|
||||
>
|
||||
<el-button text @click="editContent(data)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip v-else effect="dark" content="修改标注" placement="top">
|
||||
<el-button text @click="editMark(data)">
|
||||
<AppIcon iconName="app-document-active" class="primary"></AppIcon>
|
||||
<el-tooltip v-else effect="dark" content="修改标注" placement="top">
|
||||
<el-button text @click="editMark(data)">
|
||||
<AppIcon iconName="app-document-active" class="primary"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-divider direction="vertical" v-if="buttonData?.vote_status !== '-1'" />
|
||||
<el-button text disabled v-if="buttonData?.vote_status === '0'">
|
||||
<AppIcon iconName="app-like-color"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-divider direction="vertical" v-if="buttonData?.vote_status !== '-1'" />
|
||||
<el-button text disabled v-if="buttonData?.vote_status === '0'">
|
||||
<AppIcon iconName="app-like-color"></AppIcon>
|
||||
</el-button>
|
||||
|
||||
<el-button text disabled v-if="buttonData?.vote_status === '1'">
|
||||
<AppIcon iconName="app-oppose-color"></AppIcon>
|
||||
</el-button>
|
||||
<EditContentDialog ref="EditContentDialogRef" @refresh="refreshContent" />
|
||||
<EditMarkDialog ref="EditMarkDialogRef" @refresh="refreshMark" />
|
||||
<!-- 先渲染,不然不能播放 -->
|
||||
<audio ref="audioPlayer" controls hidden="hidden"></audio>
|
||||
<el-button text disabled v-if="buttonData?.vote_status === '1'">
|
||||
<AppIcon iconName="app-oppose-color"></AppIcon>
|
||||
</el-button>
|
||||
<EditContentDialog ref="EditContentDialogRef" @refresh="refreshContent" />
|
||||
<EditMarkDialog ref="EditMarkDialogRef" @refresh="refreshMark" />
|
||||
<!-- 先渲染,不然不能播放 -->
|
||||
<audio ref="audioPlayer" controls hidden="hidden"></audio>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div>
|
||||
<LogOperationButton
|
||||
v-if="type === 'log'"
|
||||
v-bind:data="chatRecord"
|
||||
:applicationId="application.id"
|
||||
:tts="application.tts_model_enable"
|
||||
:tts_type="application.tts_type"
|
||||
:type="type"
|
||||
/>
|
||||
|
||||
<div class="flex-between mt-8" v-else>
|
||||
<div>
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="chatRecord.is_stop && !chatRecord.write_ed"
|
||||
@click="startChat(chatRecord)"
|
||||
link
|
||||
>继续
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
v-else-if="!chatRecord.write_ed"
|
||||
@click="stopChat(chatRecord)"
|
||||
link
|
||||
>停止回答
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="chatRecord.write_ed && 500 != chatRecord.status" class="flex-between">
|
||||
<ChatOperationButton
|
||||
:tts="application.tts_model_enable"
|
||||
:tts_type="application.tts_type"
|
||||
:data="chatRecord"
|
||||
:type="type"
|
||||
:applicationId="application.id"
|
||||
:chatId="chatRecord.chat_id"
|
||||
:chat_loading="loading"
|
||||
@regeneration="regenerationChart(chatRecord)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import ChatOperationButton from '@/components/ai-chat/component/operation-button/ChatOperationButton.vue'
|
||||
import LogOperationButton from '@/components/ai-chat/component/operation-button/LogOperationButton.vue'
|
||||
import { type chatType } from '@/api/type/application'
|
||||
defineProps<{
|
||||
type: 'log' | 'ai-chat' | 'debug-ai-chat'
|
||||
chatRecord: chatType
|
||||
application: any
|
||||
loading: boolean
|
||||
startChat: (chat_record: any) => void
|
||||
stopChat: (chat_record: any) => void
|
||||
regenerationChart: (chat_record: any) => void
|
||||
}>()
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<!-- 开场白组件 -->
|
||||
<div class="item-content mb-16">
|
||||
<div class="avatar">
|
||||
<img v-if="application.avatar" :src="application.avatar" height="30px" />
|
||||
<LogoIcon v-else height="30px" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-card shadow="always" class="dialog-card">
|
||||
<MdRenderer :source="prologue" :send-message="sendMessage"></MdRenderer>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { type chatType } from '@/api/type/application'
|
||||
import { computed } from 'vue'
|
||||
import MdRenderer from '@/components/markdown/MdRenderer.vue'
|
||||
const props = defineProps<{
|
||||
application: any
|
||||
available: boolean
|
||||
type: 'log' | 'ai-chat' | 'debug-ai-chat'
|
||||
sendMessage: (question: string, other_params_data?: any, chat?: chatType) => void
|
||||
}>()
|
||||
const toQuickQuestion = (match: string, offset: number, input: string) => {
|
||||
return `<quick_question>${match.replace('- ', '')}</quick_question>`
|
||||
}
|
||||
const prologue = computed(() => {
|
||||
const temp = props.available
|
||||
? props.application?.prologue
|
||||
: '抱歉,当前正在维护,无法提供服务,请稍后再试!'
|
||||
return temp.replace(/-\s.+/g, toQuickQuestion)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<!-- 问题内容 -->
|
||||
<div class="item-content mb-16 lighter">
|
||||
<div class="avatar">
|
||||
<el-image
|
||||
v-if="application.user_avatar"
|
||||
:src="application.user_avatar"
|
||||
alt=""
|
||||
fit="cover"
|
||||
style="width: 30px; height: 30px; display: block"
|
||||
/>
|
||||
<AppAvatar v-else>
|
||||
<img src="@/assets/user-icon.svg" style="width: 54%" alt="" />
|
||||
</AppAvatar>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="text break-all pre-wrap">
|
||||
{{ chatRecord.problem_text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { type chatType } from '@/api/type/application'
|
||||
defineProps<{
|
||||
application: any
|
||||
chatRecord: chatType
|
||||
}>()
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ai-chat {
|
||||
&__content {
|
||||
padding-top: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.avatar {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-left: var(--padding-left);
|
||||
|
||||
:deep(ol) {
|
||||
margin-left: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.problem-button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--app-layout-bg-color);
|
||||
height: 46px;
|
||||
padding: 0 12px;
|
||||
line-height: 46px;
|
||||
box-sizing: border-box;
|
||||
color: var(--el-text-color-regular);
|
||||
-webkit-line-clamp: 1;
|
||||
word-break: break-all;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
&:hover {
|
||||
background: var(--app-layout-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="
|
||||
(inputFieldList.length > 0 || (type === 'debug-ai-chat' && apiInputFieldList.length > 0)) &&
|
||||
type !== 'log'
|
||||
"
|
||||
class="mb-16"
|
||||
style="padding: 0 24px"
|
||||
>
|
||||
<el-card shadow="always" class="dialog-card">
|
||||
<div class="flex align-center cursor w-full" @click="showUserInput = !showUserInput">
|
||||
<el-icon class="mr-8 arrow-icon" :class="showUserInput ? 'rotate-90' : ''"
|
||||
><CaretRight
|
||||
/></el-icon>
|
||||
用户输入
|
||||
</div>
|
||||
<el-collapse-transition>
|
||||
<div v-show="showUserInput" class="mt-16">
|
||||
<DynamicsForm
|
||||
:key="dynamicsFormRefresh"
|
||||
v-model="form_data_context"
|
||||
:model="form_data_context"
|
||||
label-position="left"
|
||||
require-asterisk-position="right"
|
||||
:render_data="inputFieldList"
|
||||
ref="dynamicsFormRef"
|
||||
/>
|
||||
<DynamicsForm
|
||||
v-if="type === 'debug-ai-chat'"
|
||||
v-model="api_form_data_context"
|
||||
:model="api_form_data_context"
|
||||
label-position="left"
|
||||
require-asterisk-position="right"
|
||||
:render_data="apiInputFieldList"
|
||||
ref="dynamicsFormRef2"
|
||||
/>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import DynamicsForm from '@/components/dynamics-form/index.vue'
|
||||
import type { FormField } from '@/components/dynamics-form/type'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { MsgWarning } from '@/utils/message'
|
||||
const route = useRoute()
|
||||
const props = defineProps<{
|
||||
application: any
|
||||
type: 'log' | 'ai-chat' | 'debug-ai-chat'
|
||||
api_form_data: any
|
||||
form_data: any
|
||||
}>()
|
||||
// 用于刷新动态表单
|
||||
const dynamicsFormRefresh = ref(0)
|
||||
const inputFieldList = ref<FormField[]>([])
|
||||
const apiInputFieldList = ref<FormField[]>([])
|
||||
const showUserInput = ref(true)
|
||||
const emit = defineEmits(['update:api_form_data', 'update:form_data'])
|
||||
|
||||
const api_form_data_context = computed({
|
||||
get: () => {
|
||||
return props.api_form_data
|
||||
},
|
||||
set: (data) => {
|
||||
emit('update:api_form_data', data)
|
||||
}
|
||||
})
|
||||
|
||||
const form_data_context = computed({
|
||||
get: () => {
|
||||
return props.form_data
|
||||
},
|
||||
set: (data) => {
|
||||
emit('update:form_data', data)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.application,
|
||||
() => {
|
||||
handleInputFieldList()
|
||||
}
|
||||
)
|
||||
|
||||
function handleInputFieldList() {
|
||||
dynamicsFormRefresh.value++
|
||||
let default_value: any = {}
|
||||
props.application.work_flow?.nodes
|
||||
?.filter((v: any) => v.id === 'base-node')
|
||||
.map((v: any) => {
|
||||
inputFieldList.value = v.properties.user_input_field_list
|
||||
? v.properties.user_input_field_list.map((v: any) => {
|
||||
switch (v.type) {
|
||||
case 'input':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'TextInput',
|
||||
label: v.name,
|
||||
default_value: default_value[v.variable],
|
||||
required: v.is_required
|
||||
}
|
||||
case 'select':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'SingleSelect',
|
||||
label: v.name,
|
||||
default_value: default_value[v.variable],
|
||||
required: v.is_required,
|
||||
option_list: v.optionList.map((o: any) => {
|
||||
return { key: o, value: o }
|
||||
})
|
||||
}
|
||||
case 'date':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'DatePicker',
|
||||
label: v.name,
|
||||
default_value: default_value[v.variable],
|
||||
required: v.is_required,
|
||||
attrs: {
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
'value-format': 'YYYY-MM-DD HH:mm:ss',
|
||||
type: 'datetime'
|
||||
}
|
||||
}
|
||||
default:
|
||||
return v
|
||||
}
|
||||
})
|
||||
: v.properties.input_field_list
|
||||
? v.properties.input_field_list
|
||||
.filter((v: any) => v.assignment_method === 'user_input')
|
||||
.map((v: any) => {
|
||||
switch (v.type) {
|
||||
case 'input':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'TextInput',
|
||||
label: v.name,
|
||||
default_value: default_value[v.variable],
|
||||
required: v.is_required
|
||||
}
|
||||
case 'select':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'SingleSelect',
|
||||
label: v.name,
|
||||
default_value: default_value[v.variable],
|
||||
required: v.is_required,
|
||||
option_list: v.optionList.map((o: any) => {
|
||||
return { key: o, value: o }
|
||||
})
|
||||
}
|
||||
case 'date':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'DatePicker',
|
||||
label: v.name,
|
||||
default_value: default_value[v.variable],
|
||||
required: v.is_required,
|
||||
attrs: {
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
'value-format': 'YYYY-MM-DD HH:mm:ss',
|
||||
type: 'datetime'
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
: []
|
||||
|
||||
apiInputFieldList.value = v.properties.api_input_field_list
|
||||
? v.properties.api_input_field_list.map((v: any) => {
|
||||
switch (v.type) {
|
||||
case 'input':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'TextInput',
|
||||
label: v.variable,
|
||||
default_value: v.default_value || default_value[v.variable],
|
||||
required: v.is_required
|
||||
}
|
||||
case 'select':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'SingleSelect',
|
||||
label: v.variable,
|
||||
default_value: v.default_value || default_value[v.variable],
|
||||
required: v.is_required,
|
||||
option_list: v.optionList.map((o: any) => {
|
||||
return { key: o, value: o }
|
||||
})
|
||||
}
|
||||
case 'date':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'DatePicker',
|
||||
label: v.variable,
|
||||
default_value: v.default_value || default_value[v.variable],
|
||||
required: v.is_required,
|
||||
attrs: {
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
'value-format': 'YYYY-MM-DD HH:mm:ss',
|
||||
type: 'datetime'
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
: v.properties.input_field_list
|
||||
? v.properties.input_field_list
|
||||
.filter((v: any) => v.assignment_method === 'api_input')
|
||||
.map((v: any) => {
|
||||
switch (v.type) {
|
||||
case 'input':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'TextInput',
|
||||
label: v.name,
|
||||
default_value: default_value[v.variable],
|
||||
required: v.is_required
|
||||
}
|
||||
case 'select':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'SingleSelect',
|
||||
label: v.name,
|
||||
default_value: default_value[v.variable],
|
||||
required: v.is_required,
|
||||
option_list: v.optionList.map((o: any) => {
|
||||
return { key: o, value: o }
|
||||
})
|
||||
}
|
||||
case 'date':
|
||||
return {
|
||||
field: v.variable,
|
||||
input_type: 'DatePicker',
|
||||
label: v.name,
|
||||
default_value: default_value[v.variable],
|
||||
required: v.is_required,
|
||||
attrs: {
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
'value-format': 'YYYY-MM-DD HH:mm:ss',
|
||||
type: 'datetime'
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
: []
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 校验参数
|
||||
*/
|
||||
const checkInputParam = () => {
|
||||
// 检查inputFieldList是否有未填写的字段
|
||||
for (let i = 0; i < inputFieldList.value.length; i++) {
|
||||
if (
|
||||
inputFieldList.value[i].required &&
|
||||
!form_data_context.value[inputFieldList.value[i].field]
|
||||
) {
|
||||
MsgWarning('请填写所有必填字段')
|
||||
return false
|
||||
}
|
||||
}
|
||||
// 浏览器query参数找到接口传参
|
||||
let msg = []
|
||||
for (let f of apiInputFieldList.value) {
|
||||
if (!api_form_data_context.value[f.field]) {
|
||||
api_form_data_context.value[f.field] = route.query[f.field]
|
||||
}
|
||||
if (f.required && !api_form_data_context.value[f.field]) {
|
||||
msg.push(f.field)
|
||||
}
|
||||
}
|
||||
if (msg.length > 0) {
|
||||
MsgWarning(`请在URL中填写参数 ${msg.join('、')}的值`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
defineExpose({ checkInputParam })
|
||||
onMounted(() => {
|
||||
handleInputFieldList()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.chat-width {
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.chat-width {
|
||||
max-width: 100% !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -154,7 +154,7 @@ const initDefaultData = (formField: FormField) => {
|
|||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
render(props.render_data, {})
|
||||
render(props.render_data, props.modelValue)
|
||||
})
|
||||
|
||||
const render = (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div>
|
||||
<DynamicsForm
|
||||
:disabled="is_submit"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
ref="dynamicsFormRef"
|
||||
:render_data="form_field_list"
|
||||
label-suffix=":"
|
||||
v-model="form_data"
|
||||
:model="form_data"
|
||||
></DynamicsForm>
|
||||
<el-button :type="is_submit ? 'info' : 'primary'" :disabled="is_submit" @click="submit"
|
||||
>提交</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import DynamicsForm from '@/components/dynamics-form/index.vue'
|
||||
const props = defineProps<{
|
||||
form_setting: string
|
||||
sendMessage: (question: string, type: 'old' | 'new', other_params_data?: any) => void
|
||||
}>()
|
||||
const form_setting_data = computed(() => {
|
||||
if (props.form_setting) {
|
||||
return JSON.parse(props.form_setting)
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
const _submit = ref<boolean>(false)
|
||||
/**
|
||||
* 表单字段列表
|
||||
*/
|
||||
const form_field_list = computed(() => {
|
||||
if (form_setting_data.value.form_field_list) {
|
||||
return form_setting_data.value.form_field_list
|
||||
}
|
||||
return []
|
||||
})
|
||||
const is_submit = computed(() => {
|
||||
if (_submit.value) {
|
||||
return true
|
||||
}
|
||||
if (form_setting_data.value.is_submit) {
|
||||
return form_setting_data.value.is_submit
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const _form_data = ref<any>({})
|
||||
const form_data = computed({
|
||||
get: () => {
|
||||
console.log(form_setting_data.value)
|
||||
if (form_setting_data.value.is_submit) {
|
||||
return form_setting_data.value.form_data
|
||||
} else {
|
||||
return _form_data.value
|
||||
}
|
||||
},
|
||||
set: (v) => {
|
||||
_form_data.value = v
|
||||
}
|
||||
})
|
||||
const dynamicsFormRef = ref<InstanceType<typeof DynamicsForm>>()
|
||||
const submit = () => {
|
||||
dynamicsFormRef.value?.validate().then(() => {
|
||||
_submit.value = true
|
||||
const setting = JSON.parse(props.form_setting)
|
||||
props.sendMessage('', 'old', {
|
||||
runtime_node_id: setting.runtime_node_id,
|
||||
chat_record_id: setting.chat_record_id,
|
||||
node_data: form_data.value
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
<template v-for="(item, index) in md_view_list" :key="index">
|
||||
<div
|
||||
v-if="item.type === 'question'"
|
||||
@click="quickProblemHandle ? quickProblemHandle(item.content) : (content: string) => {}"
|
||||
@click="sendMessage ? sendMessage(item.content, 'new') : (content: string) => {}"
|
||||
class="problem-button ellipsis-2 mb-8"
|
||||
:class="quickProblemHandle ? 'cursor' : 'disabled'"
|
||||
:class="sendMessage ? 'cursor' : 'disabled'"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
{{ item.content }}
|
||||
|
|
@ -14,6 +14,11 @@
|
|||
v-else-if="item.type === 'echarts_rander'"
|
||||
:option="item.content"
|
||||
></EchartsRander>
|
||||
<FormRander
|
||||
:sendMessage="sendMessage"
|
||||
v-else-if="item.type === 'form_rander'"
|
||||
:form_setting="item.content"
|
||||
></FormRander>
|
||||
<MdPreview
|
||||
v-else
|
||||
noIconfont
|
||||
|
|
@ -30,6 +35,7 @@ import { computed, ref } from 'vue'
|
|||
import { config } from 'md-editor-v3'
|
||||
import HtmlRander from './HtmlRander.vue'
|
||||
import EchartsRander from './EchartsRander.vue'
|
||||
import FormRander from './FormRander.vue'
|
||||
config({
|
||||
markdownItConfig(md) {
|
||||
md.renderer.rules.image = (tokens, idx, options, env, self) => {
|
||||
|
|
@ -54,7 +60,7 @@ const props = withDefaults(
|
|||
defineProps<{
|
||||
source?: string
|
||||
inner_suffix?: boolean
|
||||
quickProblemHandle?: (q: string) => void
|
||||
sendMessage?: (question: string, type: 'old' | 'new', other_params_data?: any) => void
|
||||
}>(),
|
||||
{
|
||||
source: ''
|
||||
|
|
@ -63,7 +69,9 @@ const props = withDefaults(
|
|||
const editorRef = ref()
|
||||
const md_view_list = computed(() => {
|
||||
const temp_source = props.source
|
||||
return split_echarts_rander(split_html_rander(split_quick_question([temp_source])))
|
||||
return split_form_rander(
|
||||
split_echarts_rander(split_html_rander(split_quick_question([temp_source])))
|
||||
)
|
||||
})
|
||||
|
||||
const split_quick_question = (result: Array<string>) => {
|
||||
|
|
@ -168,6 +176,41 @@ const split_echarts_rander_ = (source: string, type: string) => {
|
|||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const split_form_rander = (result: Array<any>) => {
|
||||
return result
|
||||
.map((item) => split_form_rander_(item.content, item.type))
|
||||
.reduce((x: any, y: any) => {
|
||||
return [...x, ...y]
|
||||
}, [])
|
||||
}
|
||||
|
||||
const split_form_rander_ = (source: string, type: string) => {
|
||||
const temp_md_quick_question_list = source.match(/<form_rander>[\d\D]*?<\/form_rander>/g)
|
||||
const md_quick_question_list = temp_md_quick_question_list
|
||||
? temp_md_quick_question_list.filter((i) => i)
|
||||
: []
|
||||
const split_quick_question_value = source
|
||||
.split(/<form_rander>[\d\D]*?<\/form_rander>/g)
|
||||
.filter((item) => item !== undefined)
|
||||
.filter((item) => !md_quick_question_list?.includes(item))
|
||||
const result = Array.from(
|
||||
{ length: md_quick_question_list.length + split_quick_question_value.length },
|
||||
(v, i) => i
|
||||
).map((index) => {
|
||||
if (index % 2 == 0) {
|
||||
return { type: type, content: split_quick_question_value[Math.floor(index / 2)] }
|
||||
} else {
|
||||
return {
|
||||
type: 'form_rander',
|
||||
content: md_quick_question_list[Math.floor(index / 2)]
|
||||
.replace('<form_rander>', '')
|
||||
.replace('</form_rander>', '')
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.problem-button {
|
||||
|
|
|
|||
|
|
@ -12,4 +12,5 @@ export enum WorkflowType {
|
|||
Application = 'application-node',
|
||||
DocumentExtractNode = 'document-extract-node',
|
||||
ImageUnderstandNode = 'image-understand-node',
|
||||
FormNode = 'form-node'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="scrollbar-height">
|
||||
<AiChat :data="detail" :debug="true"></AiChat>
|
||||
<AiChat :application-details="detail" :type="'debug-ai-chat'"></AiChat>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { isAppIcon } from '@/utils/application'
|
||||
import { hexToRgba } from '@/utils/theme'
|
||||
|
||||
const auth_components: any = import.meta.glob('@/views/chat/auth/component/*.vue', {
|
||||
eager: true
|
||||
|
|
@ -40,9 +39,10 @@ const auth_components: any = import.meta.glob('@/views/chat/auth/component/*.vue
|
|||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ modelValue: boolean; application_profile: any; auth_type?: string }>(),
|
||||
defineProps<{ modelValue: boolean; application_profile: any; auth_type?: string; style?: any }>(),
|
||||
{
|
||||
auth_type: 'password'
|
||||
auth_type: 'password',
|
||||
style: {}
|
||||
}
|
||||
)
|
||||
const is_auth = computed({
|
||||
|
|
@ -58,7 +58,8 @@ const customStyle = computed(() => {
|
|||
return {
|
||||
background: props.application_profile?.custom_theme?.theme_color,
|
||||
color: props.application_profile?.custom_theme?.header_font_color,
|
||||
border: 'none'
|
||||
border: 'none',
|
||||
...props.style
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@
|
|||
</div>
|
||||
<div class="chat__main chat-width">
|
||||
<AiChat
|
||||
v-model:data="applicationDetail"
|
||||
v-model:applicationDetails="applicationDetail"
|
||||
type="ai-chat"
|
||||
:available="applicationAvailable"
|
||||
:appId="applicationDetail?.id"
|
||||
></AiChat>
|
||||
|
|
|
|||
|
|
@ -34,11 +34,12 @@
|
|||
<div class="chat-embed__main">
|
||||
<AiChat
|
||||
ref="AiChatRef"
|
||||
v-model:data="applicationDetail"
|
||||
v-model:applicationDetails="applicationDetail"
|
||||
:available="applicationAvailable"
|
||||
:appId="applicationDetail?.id"
|
||||
:record="currentRecordList"
|
||||
:chatId="currentChatId"
|
||||
type="ai-chat"
|
||||
@refresh="refresh"
|
||||
@scroll="handleScroll"
|
||||
class="AiChat-embed"
|
||||
|
|
|
|||
|
|
@ -117,8 +117,9 @@
|
|||
<div class="right-height">
|
||||
<AiChat
|
||||
ref="AiChatRef"
|
||||
v-model:data="applicationDetail"
|
||||
v-model:applicationDetails="applicationDetail"
|
||||
:available="applicationAvailable"
|
||||
type="ai-chat"
|
||||
:appId="applicationDetail?.id"
|
||||
:record="currentRecordList"
|
||||
:chatId="currentChatId"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
@load="getChatRecord"
|
||||
:loading="loading"
|
||||
>
|
||||
<AiChat :data="application" :record="recordList" log></AiChat>
|
||||
<AiChat :application-details="application" :record="recordList" type="log"></AiChat>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
<template #footer>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
>
|
||||
<DynamicsFormConstructor
|
||||
v-model="dynamicsFormData"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
ref="dynamicsFormConstructorRef"
|
||||
></DynamicsFormConstructor>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
|
||||
<el-button type="primary" @click="submit()" :loading="loading"> 添加 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DynamicsFormConstructor from '@/components/dynamics-form/constructor/index.vue'
|
||||
const props = withDefaults(
|
||||
defineProps<{ title?: string; addFormField: (form_data: any) => void }>(),
|
||||
{ title: '添加参数' }
|
||||
)
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const dynamicsFormConstructorRef = ref<InstanceType<typeof DynamicsFormConstructor>>()
|
||||
const emit = defineEmits(['submit'])
|
||||
const dynamicsFormData = ref<any>({})
|
||||
const loading = ref<boolean>(false)
|
||||
const open = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
dynamicsFormData.value = {}
|
||||
}
|
||||
const submit = () => {
|
||||
dynamicsFormConstructorRef.value?.validate().then(() => {
|
||||
props.addFormField(dynamicsFormConstructorRef.value?.getData())
|
||||
close()
|
||||
})
|
||||
}
|
||||
defineExpose({ close, open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
>
|
||||
<DynamicsFormConstructor
|
||||
v-model="dynamicsFormData"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
ref="dynamicsFormConstructorRef"
|
||||
></DynamicsFormConstructor>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
|
||||
<el-button type="primary" @click="submit()" :loading="loading"> 修改 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DynamicsFormConstructor from '@/components/dynamics-form/constructor/index.vue'
|
||||
const props = withDefaults(
|
||||
defineProps<{ title?: string; editFormField: (form_data: any, index: number) => void }>(),
|
||||
{ title: '修改参数' }
|
||||
)
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const dynamicsFormConstructorRef = ref<InstanceType<typeof DynamicsFormConstructor>>()
|
||||
const emit = defineEmits(['submit'])
|
||||
const dynamicsFormData = ref<any>({})
|
||||
const currentIndex = ref<number>(0)
|
||||
const loading = ref<boolean>(false)
|
||||
const open = (form_data: any, index: number) => {
|
||||
dialogVisible.value = true
|
||||
dynamicsFormData.value = form_data
|
||||
currentIndex.value = index
|
||||
}
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
dynamicsFormData.value = {}
|
||||
}
|
||||
const submit = () => {
|
||||
dynamicsFormConstructorRef.value?.validate().then(() => {
|
||||
props.editFormField(dynamicsFormConstructorRef.value?.getData(), currentIndex.value)
|
||||
close()
|
||||
})
|
||||
}
|
||||
defineExpose({ close, open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -168,6 +168,31 @@ export const rerankerNode = {
|
|||
}
|
||||
}
|
||||
}
|
||||
export const formNode = {
|
||||
type: WorkflowType.FormNode,
|
||||
text: '在问答过程中用于收集用户信息,可以根据收集到表单数据执行后续流程',
|
||||
label: '表单收集',
|
||||
height: 252,
|
||||
properties: {
|
||||
width: 600,
|
||||
stepName: '表单收集',
|
||||
node_data: {
|
||||
is_result: true,
|
||||
form_field_list: [],
|
||||
form_content_format: `你好,请先填写下面表单内容:
|
||||
{{form}}
|
||||
填写后请点击【提交】按钮进行提交。`
|
||||
},
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: '表单全部内容',
|
||||
value: 'form_data'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const documentExtractNode = {
|
||||
type: WorkflowType.DocumentExtractNode,
|
||||
text: '提取文档中的内容',
|
||||
|
|
@ -180,7 +205,7 @@ export const documentExtractNode = {
|
|||
{
|
||||
label: '文件内容',
|
||||
value: 'content'
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -197,7 +222,7 @@ export const imageUnderstandNode = {
|
|||
{
|
||||
label: 'AI 回答内容',
|
||||
value: 'content'
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -208,9 +233,7 @@ export const menuNodes = [
|
|||
questionNode,
|
||||
conditionNode,
|
||||
replyNode,
|
||||
rerankerNode,
|
||||
documentExtractNode,
|
||||
imageUnderstandNode
|
||||
rerankerNode
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
@ -297,9 +320,10 @@ export const nodeDict: any = {
|
|||
[WorkflowType.FunctionLib]: functionLibNode,
|
||||
[WorkflowType.FunctionLibCustom]: functionNode,
|
||||
[WorkflowType.RrerankerNode]: rerankerNode,
|
||||
[WorkflowType.FormNode]: formNode,
|
||||
[WorkflowType.Application]: applicationNode,
|
||||
[WorkflowType.DocumentExtractNode]: documentExtractNode,
|
||||
[WorkflowType.ImageUnderstandNode]: imageUnderstandNode,
|
||||
[WorkflowType.ImageUnderstandNode]: imageUnderstandNode
|
||||
}
|
||||
export function isWorkFlow(type: string | undefined) {
|
||||
return type === 'WORK_FLOW'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #34c724">
|
||||
<img src="@/assets/icon_form.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import FormNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
class FormNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, FormNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'form-node',
|
||||
model: AppNodeModel,
|
||||
view: FormNode
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
label-width="auto"
|
||||
ref="formNodeFormRef"
|
||||
hide-required-asterisk
|
||||
>
|
||||
<el-form-item
|
||||
label="表单输出内容"
|
||||
prop="form_content_format"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: '请表单输出内容',
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="mr-4">
|
||||
<span>表单输出内容<span class="danger">*</span></span>
|
||||
</div>
|
||||
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
|
||||
<template #content>
|
||||
`设置执行该节点输出的内容,{{ '{ from }' }}为表单的占位符。`
|
||||
</template>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<MdEditorMagnify
|
||||
title="表单输出内容"
|
||||
v-model="form_data.form_content_format"
|
||||
style="height: 150px"
|
||||
@submitDialog="submitDialog"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="表单配置" @click.prevent>
|
||||
<div class="flex-between mb-16">
|
||||
<h5 class="lighter">{{ '接口传参' }}</h5>
|
||||
<el-button link type="primary" @click="openAddFormCollect()">
|
||||
<el-icon class="mr-4">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="form_data.form_field_list.length > 0"
|
||||
:data="form_data.form_field_list"
|
||||
class="mb-16"
|
||||
>
|
||||
<el-table-column prop="field" label="参数" />
|
||||
<el-table-column prop="label" label="显示名称">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.label && row.label.input_type === 'TooltipLabel'">{{
|
||||
row.label.label
|
||||
}}</span>
|
||||
<span v-else>{{ row.label }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="组件类型" width="110px">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" class="info-tag">{{
|
||||
input_type_list.find((item) => item.value === row.input_type)?.label
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="default_value" label="默认值" />
|
||||
<el-table-column label="必填">
|
||||
<template #default="{ row }">
|
||||
<div @click.stop>
|
||||
<el-switch disabled size="small" v-model="row.is_required" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="left" width="80">
|
||||
<template #default="{ row, $index }">
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" content="修改" placement="top">
|
||||
<el-button type="primary" text @click.stop="openEditFormCollect(row, $index)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-tooltip effect="dark" content="删除" placement="top">
|
||||
<el-button type="primary" text @click="deleteField(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<AddFormCollect ref="addFormCollectRef" :addFormField="addFormField"></AddFormCollect>
|
||||
<EditFormCollect ref="editFormCollectRef" :editFormField="editFormField"></EditFormCollect>
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import AddFormCollect from '@/workflow/common/AddFormCollect.vue'
|
||||
import EditFormCollect from '@/workflow/common/EditFormCollect.vue'
|
||||
import { type FormInstance } from 'element-plus'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { input_type_list } from '@/components/dynamics-form/constructor/data'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import { set } from 'lodash'
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
const formNodeFormRef = ref<FormInstance>()
|
||||
const editFormField = (form_field_data: any, field_index: number) => {
|
||||
form_data.value.form_field_list = form_data.value.form_field_list.map(
|
||||
(item: any, index: number) => {
|
||||
if (field_index === index) {
|
||||
return form_field_data
|
||||
}
|
||||
return item
|
||||
}
|
||||
)
|
||||
sync_form_field_list()
|
||||
}
|
||||
const addFormField = (form_field_data: any) => {
|
||||
if (form_data.value.form_field_list.some((field: any) => field.field === form_field_data.field)) {
|
||||
MsgError('参数已存在:' + form_field_data.field)
|
||||
return
|
||||
}
|
||||
form_data.value.form_field_list = [...form_data.value.form_field_list, form_field_data]
|
||||
sync_form_field_list()
|
||||
}
|
||||
const sync_form_field_list = () => {
|
||||
const fields = [
|
||||
{
|
||||
label: '表单全部内容',
|
||||
value: 'form_data'
|
||||
},
|
||||
...form_data.value.form_field_list.map((item: any) => ({
|
||||
value: item.field,
|
||||
label: typeof item.label == 'string' ? item.label : item.label.label
|
||||
}))
|
||||
]
|
||||
set(props.nodeModel.properties.config, 'fields', fields)
|
||||
}
|
||||
const addFormCollectRef = ref<InstanceType<typeof AddFormCollect>>()
|
||||
const editFormCollectRef = ref<InstanceType<typeof EditFormCollect>>()
|
||||
const openAddFormCollect = () => {
|
||||
addFormCollectRef.value?.open()
|
||||
}
|
||||
const openEditFormCollect = (form_field_data: any, index: number) => {
|
||||
editFormCollectRef.value?.open(form_field_data, index)
|
||||
}
|
||||
const deleteField = (form_field_data: any) => {
|
||||
form_data.value.form_field_list = form_data.value.form_field_list.filter(
|
||||
(field: any) => field.field !== form_field_data.field
|
||||
)
|
||||
}
|
||||
const form = ref<any>({
|
||||
is_result: true,
|
||||
form_content_format: `你好,请先填写下面表单内容:
|
||||
{{form}}
|
||||
填写后请点击【提交】按钮进行提交。`,
|
||||
form_field_list: []
|
||||
})
|
||||
const form_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form.value)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
}
|
||||
})
|
||||
const validate = () => {
|
||||
return formNodeFormRef.value?.validate()
|
||||
}
|
||||
function submitDialog(val: string) {
|
||||
set(props.nodeModel.properties.node_data, 'form_content_format', val)
|
||||
}
|
||||
onMounted(() => {
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
sync_form_field_list()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
Loading…
Reference in New Issue