You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1053 lines
49 KiB

1 month ago
import datetime
import json
import uuid
import dash
from dash import set_props, html, dcc
from flask import session, request
import feffery_antd_components as fac
import feffery_utils_components as fuc
import feffery_markdown_components as fmc
from dash.dependencies import Input, Output, State, MATCH, ClientsideFunction, ALL
from config.global_config import AIChatConfig, ApiBaseUrlConfig
from config.env import AppConfig
from api.aichat import *
from server import app
# todo 删除按钮未测, 查询历史记录 去除当前session_id
app.clientside_callback(
# 处理流式回复内容更新相关过程
ClientsideFunction(namespace="aichat_clientside", function_name="handleStreamResponse"),
[Output("chat_data_list", "data", allow_duplicate=True),
Output("answer_list", "data", allow_duplicate=True)],
Input({"type": "assistant-output-sse", "index": ALL}, "data"),
[State("chat_data_list", "data"),
State("current_problem", "data"),
State("answer_list", "data")],
prevent_initial_call=True,
)
app.clientside_callback(
# 控制用户信息输入框内容的发送
ClientsideFunction(
namespace="aichat_clientside", function_name="handleUserNewMessageSend"
),
[Output("chat-textarea", "value"),
Output("current_problem", "data"),
Output("chat_data_list", "data", allow_duplicate=True),
Output("current_files", "data", allow_duplicate=True)
],
[
Input("control-enter-keypress", "pressedCounts"),
Input("aichat_enter-keypress", "pressedCounts"),
Input("sent-button", "nClicks")
],
[State("chat-textarea", "value"),
State("chat_data_list", "data"),
State("current_files", "data")],
prevent_initial_call=True,
)
@app.callback([Output("chat_list_div", "children", allow_duplicate=True),
Output("current_problem", "data", allow_duplicate=True),
Output("chat_data_list", "data", allow_duplicate=True),
Output("chat_session_id", "data", allow_duplicate=True)
],
Input("new-chat-window", "nClicks"),
prevent_initial_call=True)
def new_chat_button_click(nClicks):
return [], "", [], uuid.uuid4()
@app.callback(
Output("chat_data_list", "data", allow_duplicate=True),
[Input({"type": "stop_eventsource", "index": ALL}, "nClicks"),
Input({"type": "continue_eventsource", "index": ALL}, "nClicks")],
[State("chat_data_list", "data"),
State("answer_list", "data")],
prevent_initial_call=True
)
def eventsource_operate(nClick1, nClick2, data, answer_list):
if (nClick1 is not None and dash.ctx.triggered_id is not None and 'type' in dash.ctx.triggered_id and
dash.ctx.triggered_id['type'] == 'stop_eventsource'
and (dash.ctx.triggered[0].get("value"))):
index = dash.ctx.triggered_id['index']
data[index]['is_stop'] = True
return data
if (nClick2 is not None and dash.ctx.triggered_id is not None and 'type' in dash.ctx.triggered_id and
dash.ctx.triggered_id['type'] == 'continue_eventsource'
and (dash.ctx.triggered[0].get("value"))):
index = dash.ctx.triggered_id['index']
data[index]['is_stop'] = False
for item in answer_list:
chat_index = item['index']
if chat_index == index:
data[index]['content'] = item['content']
data[index]['is_end'] = item['is_end']
return data
return dash.no_update
@app.callback(Input("answer_list", "data"),
[State("chat_data_list", "data"),
State("chat_session_id", "data")],
prevent_initial_call=True)
def sync_chat_data(answer_list, chat_data_list, chat_session_id):
if answer_list and len(answer_list) > 0 and 'is_end' in answer_list[-1] and answer_list[-1]['is_end']:
question = chat_data_list[len(chat_data_list) - 2]
question_json = dict(
chat_id=None, session_id=chat_session_id, session_name=chat_data_list[0]['content'][:20],
type=question['type'], is_end=None, is_stop=None, copy_text=None, user=None,
time=question['time'] if 'time' in question else None,
content=question['content'], operate=None, thumb_down_reason=None,
file=question['file'] if 'file' in question else None
)
answer = chat_data_list[-1]
answer_json = dict(
chat_id=None, session_id=chat_session_id, session_name=chat_data_list[0]['content'][:20],
type=answer['type'], is_end=answer['is_end'], is_stop=answer['is_stop'], copy_text=answer['copy_text'],
user=None, time=answer['time'] if 'time' in answer else None,
content=json.dumps(answer['content']), operate=answer['operate'] if 'operate' in answer else None,
thumb_down_reason=answer['thumb_down_reason'] if 'thumb_down_reason' in answer else None,
file=answer['file'] if 'file' in answer else None
)
add_chat(question_json)
answer['content'] = json.dumps(answer['content'])
add_chat(answer_json)
@app.callback([Output("chat_list_div", "children"),
Output("new-chat-window", "disabled")],
[Input("chat_data_list", "data")],
[State("current_problem", "data"),
State("chat_session_id", "data"),
State("current_machine", "data")],
prevent_initial_call=True)
def refresh_chat_window_by_chat_list(data, current_problem, session_id, current_machine):
if data is not None and len(data) > 0:
children = []
for index, item in enumerate(data):
if item['type'] == 'question' and item['content'] is not None:
question_children = html.Div(
[
fuc.FefferyPostEventSource(
id={
"type": "assistant-output-sse",
"index": index,
},
headers={},
url=AIChatConfig.AIChatUrl,
body=json.dumps({
"query": item['content'],
"user_id": session.get('user_info').get('user_name'),
"robot": current_machine,
"session_id": session_id,
"doc": json.loads(item['file']) if 'file' in item else [],
"history": []
}),
autoReconnect={"retries": 0, "delay": 0},
),
fac.AntdAvatar(
icon="antd-user",
mode="icon",
className='avatar',
style={
"float": "left",
"height": "30px",
"background": "#3370FF",
}
),
html.Div(
html.Div(
[html.Div(item['content']),
fac.AntdSpace(
[
fac.AntdButton(
fac.AntdTag(
content=file['file'] if len(file['file']) < 30
else (file['file'][:30] + "...")
, style={
'color': 'rgb(63 215 121)',
'background-color': 'rgb(230, 255, 239)',
'border-color': 'rgb(102, 255, 161)'
}),
id={'type': 'file', 'index': str(index)+"_"+str(iddx)},
type='link'
) for iddx, file in enumerate(json.loads(item['file'])) if 'file' in item
],
wrap=True,
), ],
style={
"padding": '6px 0',
"word-break": 'break-all',
"white-space": 'pre-wrap'
}
),
style={"padding-left": '40px', "font-size": "14px"}
)
],
className="chat-list-content-lighter",
style={
"margin-bottom": '16px',
"font-weight": '400',
}
)
children.append(question_children)
if item['type'] == 'answer' and item['content'] is not None:
answer_children = html.Div(
[
fac.AntdAvatar(
mode='image',
src='/assets/imgs/logo2.png',
className='avatar',
style={
"float": "left",
"height": "30px"
}
),
html.Div([
fac.AntdCard(
[(
html.Div(
[fac.AntdTable(
columns=[{'title': key, 'dataIndex': key}
for key in (content_item['content'][0]).keys()],
data=content_item['content'],
style={"margin-top": "5px"}
),
fac.AntdButton(
id={"type": "down_load_table", "index": str(index) + "_" + str(idx)},
icon=fac.AntdIcon(icon="antd-download"),
style={"position": "absolute", "top": "10px", "right": "0px"}
)
],
style={"width": "100%", "position": "relative"}
)
if content_item['type'] == 'table'
else (
html.Div(
html.Iframe(
srcDoc=content_item['content'],
style={"width": "100%", "height": "100%", "border": "none"},
className="iframeDoc",
id={"type": "html_image",
"index": str(index) + '_' + str(idx)},
),
style={"width": "100%", "height": "320px", "margin-top": "5px"}
)
if content_item['type'] == 'html_image'
else (
html.Div(
[
fac.AntdIcon(
id={"type": "g6-full-screen-icon",
"index": str(index) + '_' + str(idx)},
icon="antd-full-screen",
style={"float": "right",
"font-size": "25px",
"cursor": "pointer"}
),
html.Div(
id={"type": "G6_ER", "index": str(index) + '_' + str(idx)},
className="G6_ER",
style={"width": "100%", "height": "100%", "overflow": "hidden"}
)],
id={"type": "G6_ER_DIV", "index": str(index) + '_' + str(idx)},
style={"width": "100%", "height": "300px", "overflow": "hidden",
"margin-top": "5px"}
)
if content_item['type'] == 'G6_ER'
else
html.Div(
fmc.FefferyMarkdown(
markdownStr=content_item['content'],
style={"font-size": "14px"}
),
style={"width": "100%", "margin-top": "5px"}
)
)
)
) for idx, content_item in enumerate(item['content'])],
style={
"box-shadow": '0px 2px 4px 0px rgba(31, 35, 41, .12)',
"border": 'none',
"border-radius": '8px',
"box-sizing": 'border-box',
},
headStyle={'display': 'none'}
),
html.Div(
html.Div(
[fac.AntdButton("继续", id={"type": "continue_eventsource", "index": index},
type="link") if 'is_stop' in item and item['is_stop']
and 'is_end' in item and not item['is_end']
else None,
fac.AntdButton("停止回答", id={"type": "stop_eventsource", "index": index},
type="link") if 'is_stop' in item and not item['is_stop']
and 'is_end' in item and not item['is_end']
else None
]
),
style={
"display": "flex",
"justify-content": "space-between",
"align-items": "center",
"margin-top": "8px"
}
),
html.Div(
[
html.Div(
fac.AntdText(
type='secondary',
children=[
html.Span(
item['date_time'],
style={"margin-left": "4px"}
)
] if 'date_time' in item
else []
)
),
html.Div(
html.Span(
children=[
fac.AntdTooltip(
title="换个答案",
zIndex=99999,
children=[
fac.AntdButton(
id={"type": "refresh_answer", "index": index},
type="text",
children=[
fac.AntdIcon(icon="antd-reload")
],
style={
"padding": "4px"
}
)
]
),
fac.AntdDivider(
direction="vertical"
),
fac.AntdTooltip(
title="复制",
zIndex=99999,
children=[
fac.AntdCopyText(
id={"type": "copy_answer", "index": index},
text=item['copy_text'],
className="chat_operate_icon",
style={
"padding": "4px",
"z-index": "99999"
},
beforeIcon=fac.AntdIcon(icon="antd-copy",
style={"color": "black"})
)
]
),
fac.AntdDivider(
direction="vertical"
),
fac.AntdTooltip(
title="取消赞同" if 'operate' in item and item[
'operate'] == 'thumb_up'
else "赞同",
zIndex=99999,
children=[
fac.AntdButton(
id={"type": "thumb_up_answer", "index": index},
type="text",
children=[
fac.AntdIcon(icon="md-thumb-up")
],
style={"padding": "4px", "color": "#E6A23C"}
if 'operate' in item and item['operate'] == 'thumb_up'
else {"padding": "4px"}
)
]
) if ('operate' in item and item['operate'] != 'thumb_down') or
('operate' not in item)
else None,
fac.AntdDivider(
direction="vertical"
) if ('operate' in item and item['operate'] != 'thumb_down') or
('operate' not in item)
else None,
fac.AntdTooltip(
title="取消反对" if 'operate' in item and item[
'operate'] == 'thumb_down'
else "反对",
zIndex=99999,
children=[
fac.AntdButton(
id={"type": "cancel_thumb_down_answer", "index": index},
type="text",
children=[
fac.AntdIcon(icon="md-thumb-down")
],
style={"padding": "4px", "color": "#F56C6C"}
)
if 'operate' in item and item['operate'] == 'thumb_down'
else
fac.AntdPopconfirm(
fac.AntdButton(
type="text",
children=[
fac.AntdIcon(icon="md-thumb-down")
],
style={"padding": "4px"}
),
id={"type": "thumb_down_answer", "index": index},
title='反对原因',
trigger="click",
zIndex=99999,
description=[
fac.AntdInput(
id={'type': 'thumb_down_reason', "index": index},
value=item['thumb_down_reason']
if 'thumb_down_reason' in item
else ''
)
],
placement="leftBottom",
confirmCounts=0
)
]
) if ('operate' in item and item['operate'] != 'thumb_up') or
('operate' not in item)
else None
]
)
) if 'is_end' in item and item['is_end']
and 'is_stop' in item and not item['is_stop'] else html.Div()
],
style={
"display": "flex",
"justify-content": "space-between",
"align-items": "center",
}
)
],
className="content",
style={
"padding-left": '40px'
}
)
],
className="chat-list-content-lighter",
style={
"margin-bottom": '16px',
"font-weight": '400',
}
)
children.append(answer_children)
return children, False
else:
return [], True
app.clientside_callback(
# 使用目标容器id为Input,利用初次自动回调触发即时渲染
ClientsideFunction(namespace="aichat_clientside", function_name="renderG6"),
Output({"type": "G6_ER", "index": ALL}, "children"),
Input({"type": "G6_ER", "index": ALL}, "id"),
State("chat_data_list", "data"),
)
app.clientside_callback(
# 使用目标容器id为Input,利用初次自动回调触发即时渲染
ClientsideFunction(namespace="aichat_clientside", function_name="full_g6_modal"),
Output("g6-full-screen-body", "children"),
Input("g6-modal", "style"),
State("current_g6", "data"),
prevent_initial_call=True
)
@app.callback(
Output("current_g6", "data", allow_duplicate=True),
Input("g6-exit-full-screen", "nClicks"),
prevent_initial_call=True
)
def exit_full_screen(nClicks):
if nClicks is not None and nClicks > 0:
set_props("g6-modal", {"style": {
"display": 'none'
}})
@app.callback(
Output("current_g6", "data"),
Input({"type": "g6-full-screen-icon", "index": ALL}, "nClicks"),
State("chat_data_list", "data"),
prevent_initial_call=True,
)
def g6_full_screen_operate(nClicks, data):
if (nClicks is not None and dash.ctx.triggered_id is not None and 'type' in dash.ctx.triggered_id and
dash.ctx.triggered_id['type'] == 'g6-full-screen-icon'
and (dash.ctx.triggered[0].get("value"))):
index = dash.ctx.triggered_id['index']
chat_idx = index.split("_")[0]
idx = index.split("_")[1]
g6_data = data[int(chat_idx)]['content'][int(idx)]['content']
set_props("g6-modal", {"style": {
"z-index": '99999',
"width": '100%',
"height": '100%',
"position": 'absolute',
"left": '0px',
"top": '0px',
"display": 'block',
"background": "white"
}})
return g6_data
app.clientside_callback(
# 使用目标容器id为Input,利用初次自动回调触发即时渲染
ClientsideFunction(namespace="aichat_clientside", function_name="down_load_table"),
Input({"type": "down_load_table", "index": ALL}, "nClicks"),
State("chat_data_list", "data"),
prevent_initial_call=True
)
@app.callback([Output("chat_data_list", "data", allow_duplicate=True),
Output("current_problem", "data", allow_duplicate=True)],
[Input({"type": "refresh_answer", "index": ALL}, "nClicks"),
Input({"type": "thumb_up_answer", "index": ALL}, "nClicks"),
Input({"type": "thumb_down_answer", "index": ALL}, "confirmCounts"),
Input({"type": "cancel_thumb_down_answer", "index": ALL}, "nClicks")],
[State("chat_data_list", "data"),
State({'type': 'thumb_down_reason', "index": ALL}, "value")],
prevent_initial_call=True)
def click_answer_operate_callback(nClick1, nClick2, confirmCounts, nClicks3, data, values):
if (nClick1 is not None and dash.ctx.triggered_id and 'type' in dash.ctx.triggered_id
and dash.ctx.triggered_id['type'] == 'refresh_answer'
and (dash.ctx.triggered[0].get("value"))):
index = dash.ctx.triggered_id['index']
current_problem = data[index - 1]
data.append(current_problem)
return data, current_problem
if (nClick2 is not None and dash.ctx.triggered_id and 'type' in dash.ctx.triggered_id
and dash.ctx.triggered_id['type'] == 'thumb_up_answer'
and (dash.ctx.triggered[0].get("value"))):
index = dash.ctx.triggered_id['index']
if 'operate' in data[index] and data[index]['operate'] == 'thumb_up':
data[index]['operate'] = None
else:
data[index]['operate'] = 'thumb_up'
answer_json = dict(
chat_id=data[index]['chat_id'], session_id=data[index]['session_id'],
session_name=data[index]['session_name'],
type=data[index]['type'], is_end=data[index]['is_end'], is_stop=data[index]['is_stop'],
copy_text=data[index]['copy_text'],
user=data[index]['user'], time=data[index]['time'] if 'time' in data[index] else None,
content=json.dumps(data[index]['content']),
operate=data[index]['operate'] if 'operate' in data[index] else None,
thumb_down_reason=data[index]['thumb_down_reason'] if 'thumb_down_reason' in data[index] else None,
file=data[index]['file'] if 'file' in data[index] else None
)
update_chat(answer_json)
return data, dash.no_update
if (confirmCounts is not None and dash.ctx.triggered_id and 'type' in dash.ctx.triggered_id
and dash.ctx.triggered_id['type'] == 'thumb_down_answer'
and (dash.ctx.triggered[0].get("value"))):
index = dash.ctx.triggered_id['index']
value = values[int((index - 1) / 2)]
data[index]['thumb_down_reason'] = value
data[index]['operate'] = 'thumb_down'
answer_json = dict(
chat_id=data[index]['chat_id'], session_id=data[index]['session_id'],
session_name=data[index]['session_name'],
type=data[index]['type'], is_end=data[index]['is_end'], is_stop=data[index]['is_stop'],
copy_text=data[index]['copy_text'],
user=data[index]['user'], time=data[index]['time'] if 'time' in data[index] else None,
content=json.dumps(data[index]['content']),
operate=data[index]['operate'] if 'operate' in data[index] else None,
thumb_down_reason=data[index]['thumb_down_reason'] if 'thumb_down_reason' in data[index] else None,
file=data[index]['file'] if 'file' in data[index] else None
)
update_chat(answer_json)
return data, dash.no_update
if (nClicks3 is not None and dash.ctx.triggered_id and 'type' in dash.ctx.triggered_id
and dash.ctx.triggered_id['type'] == 'cancel_thumb_down_answer'
and (dash.ctx.triggered[0].get("value"))):
index = dash.ctx.triggered_id['index']
data[index]['thumb_down_reason'] = None
data[index]['operate'] = None
answer_json = dict(
chat_id=data[index]['chat_id'], session_id=data[index]['session_id'],
session_name=data[index]['session_name'],
type=data[index]['type'], is_end=data[index]['is_end'], is_stop=data[index]['is_stop'],
copy_text=data[index]['copy_text'],
user=data[index]['user'], time=data[index]['time'] if 'time' in data[index] else None,
content=json.dumps(data[index]['content']),
operate=data[index]['operate'] if 'operate' in data[index] else None,
thumb_down_reason=data[index]['thumb_down_reason'] if 'thumb_down_reason' in data[index] else None,
file=data[index]['file'] if 'file' in data[index] else None
)
update_chat(answer_json)
return data, dash.no_update
return dash.no_update, dash.no_update
@app.callback(Output('chat-history-p8', 'children'),
Input('chat-history-list', 'data'),
prevent_initial_call=True)
def refresh_chat_history_list(history_list):
div = html.Ul(
[html.Li(
[fuc.FefferyDiv(
children=html.Div(
[
html.Span(item.get("session_name")),
],
className="flex-between",
style={
"display": 'flex',
"justify-content": 'space-between',
"align-items": 'center',
"font-size": "14px",
"width": "100%",
"height": "100%"
}
),
style={"width": "90%", "height": "100%", "padding": "0", "color": "#303133"},
id={"type": "chat-history-li-button", "index": str(item['session_id'])},
),
fac.AntdButton(
icon=fac.AntdIcon(icon='md-delete',
style={"color": "rgb(100, "
"106, "
"115)"}),
type='text',
id={'type': 'delete-chat-button', 'index': item['session_id']},
style={"float": "right", "margin-top": "-26px"}
)],
style={"padding": "6px 16px", "cursor": "pointer"},
className="chat-history-li",
) for item in history_list],
style={"list-style": "none", "margin": "0px", "padding": "0px"}
)
return div
@app.callback([Output('chat-history-list', 'data'),
Output('show_chat_history', 'data')],
[Input('chat-history-show-button', 'nClicks'),
Input('chat-history-cover-child', 'nClicks')],
[State('show_chat_history', 'data'),
State("chat_session_id", "data")],
prevent_initial_call=True)
def click_chat_history(nClicks_1, nClick_2, data, session_id):
history_list = []
if nClicks_1 and nClicks_1 > 0:
history = get_chat_session_list_api(session_id)
if history['code'] == 200:
history_list = history['data'] # 后期拼接后台接口
show_chat_history = not data['show_chat_history']
return history_list, {'show_chat_history': show_chat_history}
@app.callback([Output('sent-button', 'disabled'),
Output('sent-button', 'children'),
],
Input('chat-textarea', 'value'))
def listen_chat_textarea_has_context(value):
if value:
return False, html.Img(
src="/assets/imgs/icon_send_colorful.svg",
style={
"height": '24px'
},
)
else:
return True, html.Img(
src="/assets/imgs/icon_send.svg",
style={
"height": '24px'
},
)
@app.callback(Output('operate-textarea', 'style'),
Input('chat-textarea', 'focusing')
)
def listen_chat_textarea_focus(focusing):
if focusing:
return {
"box-shadow": '0px 6px 24px 0px rgba(31, 35, 41, 0.08)',
"background-color": '#ffffff',
"border-radius": '8px',
"border": '1px solid #3370FF',
"box-sizing": 'border-box',
"display": 'flex',
"max-width": '860px',
"margin": '0 auto',
}
else:
return {
"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',
"display": 'flex',
"max-width": '860px',
"margin": '0 auto',
}
@app.callback(
[
Output("choose_machine_popover", "open"),
Output("choose_machine_popover", "permanent"),
Output("choose_machine_popover", "overlayStyle"),
],
[Input("chat-textarea", "value"),
Input("ai_chat_width", "data")],
State("choose_machine_popover", "overlayStyle"),
prevent_initial_call=True,
)
def open_agents_panel_with_at(value, width, style):
"""末尾输入新的@时,打开智能体选择面板并切换控制模式"""
style['width'] = width
if value and value.endswith("@"):
return [True, False, style]
else:
return [False, False, style]
@app.callback(
[
Output("chat-textarea", "value", allow_duplicate=True),
Output("choose_machine_popover", "open", allow_duplicate=True),
Output("selected-machine-container", "style"),
Output("selected-machine-container", "children"),
Output("current_machine", "data"),
],
Input({"type": "machine-item", "index": ALL}, "n_clicks"),
[
State("chat-textarea", "value"),
State("selected-machine-container", "style")
],
prevent_initial_call=True,
)
def handle_agent_select(_, value, container_style):
"""处理智能体选中行为需要引发的相关更新"""
return [
# 去除输入框末尾@符号
value[:-1] if value.endswith("@") else dash.no_update,
# 关闭智能体选择面板
False,
# 更新已选中智能体容器样式
{**container_style, "display": "flex"},
# 更新已选中智能体容器内容
[
fac.AntdText(
[
"",
fac.AntdTooltip(
fac.AntdButton(
dash.ctx.triggered_id["index"],
id="change-machine",
type="text",
nClicks=0, # 重置点击次数,以辅助判断
style={"fontWeight": "bold", "paddingLeft": 5, "paddingRight": 5}
),
title="更换智能体",
),
"对话",
],
style={"color": "#8f959f"},
strong=True,
),
fac.AntdButton(
id="close-selected-machine-container",
icon=fac.AntdIcon(icon="antd-close"),
type="text",
nClicks=0, # 重置点击次数,以辅助判断
),
],
dash.ctx.triggered_id["index"]
]
@app.callback(
[
Output("selected-machine-container", "style", allow_duplicate=True),
Output("current_machine", "data", allow_duplicate=True)
],
Input("close-selected-machine-container", "nClicks"),
[
State("selected-machine-container", "style"),
],
prevent_initial_call=True,
)
def handle_selected_agent_clear(nClicks, container_style):
"""处理已选中智能体清除行为需要引发的相关更新"""
if nClicks:
return {**container_style, "display": "none"}, None
return dash.no_update, dash.no_update
app.clientside_callback(
# 根据智能体选择面板展开状态,切换控制模式
"(open) => !open",
Output("choose_machine_popover", "permanent", allow_duplicate=True),
Input("choose_machine_popover", "open"),
prevent_initial_call=True,
)
app.clientside_callback(
# 点击已选中智能体名称,打开智能体选择面板
"(nClicks) => nClicks ? true : false",
Output("choose_machine_popover", "open", allow_duplicate=True),
Input("change-machine", "nClicks"),
prevent_initial_call=True,
)
app.clientside_callback(
# 通过关闭按钮点击智能体选择面板
"(nClicks) => false",
Output("choose_machine_popover", "open", allow_duplicate=True),
Input("close-agents-panel", "nClicks"),
prevent_initial_call=True,
)
@app.callback([Output('chat-history-collapse', 'isOpen'),
Output('chat-history-cover', 'style')],
Input('show_chat_history', 'data'),
prevent_initial_call=True)
def change_chat_history_popover_by_show(data):
show = data['show_chat_history']
cover_style = {
"z-index": '95',
"width": '100%',
"height": '100%',
"position": 'absolute',
"left": '0px',
"top": '0px',
"background": '#747374',
"filter": 'alpha(opacity=60)',
"opacity": 0.6,
"display": 'none'
}
if show:
cover_style['display'] = 'block'
else:
cover_style['display'] = 'none'
return show, cover_style
@app.callback([Output("chat_data_list", "data", allow_duplicate=True),
Output('show_chat_history', 'data', allow_duplicate=True),
Output("current_problem", "data", allow_duplicate=True),
Output("chat_session_id", "data", allow_duplicate=True)],
Input({'type': 'chat-history-li-button', 'index': ALL}, 'nClicks'),
[State('chat_data_list', "data"),
State('show_chat_history', 'data'),
State("current_problem", "data")
],
prevent_initial_call=True)
def choose_history_chat(nClicks, chat_data_list, show, current_problem):
if (nClicks is not None
and dash.ctx.triggered_id['type'] == 'chat-history-li-button'
and (
# 判断本次ALL批量触发是否有效
# value为回调角色的属性值,这里代表点击次数nclicks
# #正常由用户点击触发的行为,dash.ctx.triggered中第0个元素点击次数应有效
dash.ctx.triggered[0].get("value")
)):
history_id = dash.ctx.triggered_id['index']
chat_list = get_ai_chat_list_api(history_id)
if chat_list['code'] == 200:
arr = chat_list['data']
chat_data = []
for item in arr:
if item.get('type') == 'answer':
item['content'] = json.loads(item['content'])
chat_data.append(item)
chat_data_list = chat_data
return chat_data_list, {'show_chat_history': not show['show_chat_history']}, "", chat_data_list[0]['session_id']
else:
return chat_data_list, show, current_problem, dash.no_update
@app.callback(
Output("chat-history-list", "data", allow_duplicate=True),
Input({'type': 'delete-chat-button', 'index': ALL}, 'nClicks'),
[State('chat-history-list', 'data'),
State("chat_session_id", "data")],
prevent_initial_call=True
)
def listen_delete_chat_button_click(nClicks, data, current_session_id):
if nClicks is not None and dash.ctx.triggered_id['type'] == 'delete-chat-button' and \
(
# 判断本次ALL批量触发是否有效
# value为回调角色的属性值,这里代表点击次数nclicks
# #正常由用户点击触发的行为,dash.ctx.triggered中第0个元素点击次数应有效
dash.ctx.triggered[0].get("value")
):
delete_chat_session(dash.ctx.triggered_id['index'])
chat_history_list = get_chat_session_list_api(current_session_id)
history_list = []
if chat_history_list['code'] == 200:
history_list = chat_history_list['data'] # 后期拼接后台接口
return history_list
return data
app.clientside_callback(
# 处理聊天区域的自动滚动策略
ClientsideFunction(namespace="aichat_clientside", function_name="handleChatListAreaScroll"),
Input("listen-chat-data-list-height", "height")
)
# 显示用户导入弹窗及重置上传弹窗组件状态回调
@app.callback(
[Output('ai-chat-file-modal', 'visible'),
Output('ai-chat-file-modal', 'children')],
Input('ai_chat_upload', 'nClicks'),
[State('current_files', 'data'),
State("chat_session_id", "data"),
],
prevent_initial_call=True
)
def show_ai_chat_upload(nClicks, current_files, chat_session_id):
if nClicks and nClicks > 0:
upload_files = []
if len(current_files) > 0:
for index, item in enumerate(current_files):
upload_files.append({
"name": item['file'],
"status": "done",
"uid": item['file'],
"taskId": None
})
return True, html.Div(
fac.AntdDraggerUpload(
id='aichat-upload-choose',
apiUrl=f'{ApiBaseUrlConfig.BaseUrl}/aichat/upload',
apiUrlExtraParams={'session_id': chat_session_id},
headers={'Authorization': 'Bearer ' + session.get('Authorization')},
# fileTypes=['xls', 'xlsx'],
# fileListMaxLength=1,
defaultFileList=upload_files,
text='文件导入',
hint='点击或拖拽文件至此处进行上传'
),
style={
'marginTop': '10px'
}
)
return False, dash.no_update
@app.callback(
Output("current_files", "data"),
Input("aichat-upload-choose", "listUploadTaskRecord"),
State("current_files", "data"),
prevent_initial_call=True
)
def init_current_files(records, files):
array = []
if records is not None:
for item in records:
print(item)
if 'uploadResponse' in item:
uploadResponse = item['uploadResponse']
array.append({'file': uploadResponse['data']['file'], 'bucket': uploadResponse['data']['bucket']})
else:
for index, file in enumerate(files):
if item['fileName'] == file['file']:
array.append(file)
return array
return dash.no_update
@app.callback(
Output('file_tags', 'children'),
Input("current_files", "data"),
prevent_initial_call=True
)
def update_file_tags(files):
array = []
if files is not None and len(files) > 0:
for index, item in enumerate(files):
array.append(
fac.AntdTag(
content=item['file'][:30] + "..." if len(item['file']) > 30 else item['file'],
closeIcon=True,
id={
'type': 'tag-close-file',
'index': index,
},
style={
'color': 'rgb(63 215 121)',
'background-color': 'rgb(230, 255, 239)',
'border-color': 'rgb(102, 255, 161)',
'margin-bottom': '5px'
}
)
)
return array
@app.callback(
Output("current_files", "data", allow_duplicate=True),
Input({'type': 'tag-close-file', 'index': ALL}, 'closeCounts'),
State("current_files", "data"),
prevent_initial_call=True
)
def remove_current_file(closeCounts, files):
if closeCounts is not None and dash.ctx.triggered_id is not None and\
dash.ctx.triggered_id['type'] == 'tag-close-file' and \
(
# 判断本次ALL批量触发是否有效
# value为回调角色的属性值,这里代表点击次数nclicks
# #正常由用户点击触发的行为,dash.ctx.triggered中第0个元素点击次数应有效
dash.ctx.triggered[0].get("value")
):
index = dash.ctx.triggered_id['index']
del files[index]
return files
return dash.no_update
@app.callback(
Output('down_load_chat_file', 'data'),
Input({'type': 'file', 'index': ALL}, "nClicks"),
[State("chat_data_list", "data"),
State("chat_session_id", "data")],
prevent_initial_call=True
)
def down_load_file(nClicks, data, chat_session_id):
if nClicks is not None and dash.ctx.triggered_id['type'] == 'file'\
and (
dash.ctx.triggered[0].get("value")
):
index = dash.ctx.triggered_id['index']
chat_idx = index.split("_")[0]
idx = index.split("_")[1]
file = json.loads(data[int(chat_idx)]['file'])[int(idx)]
file_name = file['file']
bucket = file['bucket']
export_file_res = export_chat_file_api({'bucket': bucket, 'file': file_name, 'session_id': chat_session_id})
if export_file_res.status_code == 200:
export_user = export_file_res.content
return dcc.send_bytes(export_user, file_name)
return dash.no_update