diff --git a/README.md b/README.md index 8351f3a..30bfe42 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ pip3 install --upgrade openai ### 2.GPT-3.0 -使用的模型是 `text-davinci-003`,详情参考[官方文档]([https://platform.openai.com/docs/guides/chat](https://platform.openai.com/docs/guides/completion/introduction))。 +使用的模型是 `text-davinci-003`,详情参考[官方文档]([https://platform.openai.com/docs/guides/chat](https://platform.openai.com/docs/guides/completion/introduction))。注意,gpt3.0模型使用流式输出对话会出现提前中断的问题,请尽量使用一次性输出对话。 使用步骤和上述GPT-3.5基本相同: diff --git a/bridge/bridge.py b/bridge/bridge.py index 9e19251..f58198a 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -7,3 +7,8 @@ class Bridge(object): def fetch_reply_content(self, query, context): return model_factory.create_bot(config.conf().get("model").get("type")).reply(query, context) + + async def fetch_reply_stream(self, query, context): + bot=model_factory.create_bot(config.conf().get("model").get("type")) + async for final,response in bot.reply_text_stream(query, context): + yield final,response diff --git a/channel/channel.py b/channel/channel.py index e2617d1..5c589dd 100644 --- a/channel/channel.py +++ b/channel/channel.py @@ -29,3 +29,7 @@ class Channel(object): def build_reply_content(self, query, context=None): return Bridge().fetch_reply_content(query, context) + + async def build_reply_stream(self, query, context=None): + async for final,response in Bridge().fetch_reply_stream(query, context): + yield final,response diff --git a/channel/http/http_channel.py b/channel/http/http_channel.py index 2464f61..1c619b5 100644 --- a/channel/http/http_channel.py +++ b/channel/http/http_channel.py @@ -1,5 +1,6 @@ # encoding:utf-8 +import asyncio import json from channel.http import auth from flask import Flask, request, render_template, make_response @@ -9,8 +10,11 @@ from common import functions from config import channel_conf from config import channel_conf_val from channel.channel import Channel +from flask_socketio import SocketIO +from common import log http_app = Flask(__name__,) +socketio = SocketIO(http_app) # 自动重载模板文件 http_app.jinja_env.auto_reload = True http_app.config['TEMPLATES_AUTO_RELOAD'] = True @@ -19,6 +23,50 @@ http_app.config['TEMPLATES_AUTO_RELOAD'] = True http_app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1) +async def return_stream(data): + async for final, response in HttpChannel().handle_stream(data=data): + try: + if(final): + socketio.server.emit( + 'disconnect', {'result': response, 'final': final}, request.sid, namespace="/chat") + socketio.server.disconnect(request.sid) + else: + socketio.server.emit( + 'message', {'result': response, 'final': final}, request.sid, namespace="/chat") + except Exception as e: + socketio.server.disconnect(request.sid) + log.error("[http]emit:", e) + break + + +@socketio.on('message', namespace='/chat') +def stream(data): + log.info('message:', data) + if (auth.identify(request) == False): + client_sid = request.sid + socketio.server.disconnect(client_sid) + return + data = json.loads(data["data"]) + if (data): + img_match_prefix = functions.check_prefix( + data["msg"], channel_conf_val(const.HTTP, 'image_create_prefix')) + if img_match_prefix: + reply_text = HttpChannel().handle(data=data) + socketio.emit('message', {'result': reply_text}, namespace='/chat') + asyncio.run(return_stream(data)) + + +@socketio.on('connect', namespace='/chat') +def connect(): + log.info('connected') + socketio.emit('message', {'info': "connected"}, namespace='/chat') + + +@socketio.on('disconnect', namespace='/chat') +def disconnect(): + log.info('disconnect') + + @http_app.route("/chat", methods=['POST']) def chat(): if (auth.identify(request) == False): @@ -80,3 +128,10 @@ class HttpChannel(Channel): images += f"[]({url})\n" reply = images return reply + + async def handle_stream(self, data): + context = dict() + id = data["id"] + context['from_user_id'] = str(id) + async for final, reply in super().build_reply_stream(data["msg"], context): + yield final, reply diff --git a/channel/http/static/1.css b/channel/http/static/1.css index a53c284..7f1fe3b 100644 --- a/channel/http/static/1.css +++ b/channel/http/static/1.css @@ -1,4 +1,3 @@ - .typing_loader { width: 6px; height: 6px; @@ -11,7 +10,9 @@ left: -12px; margin: 7px 15px 6px; } -ol,pre { + +ol, +pre { background-color: #b1e3b1c4; border: 1px solid #c285e3ab; padding: 0.5rem 1.5rem 0.5rem; @@ -20,50 +21,52 @@ ol,pre { overflow-y: auto; } -pre::-webkit-scrollbar{ +pre::-webkit-scrollbar { width: 0px; - height:5px; + height: 5px; } -pre::-webkit-scrollbar-thumb{ + +pre::-webkit-scrollbar-thumb { border-right: 10px #ffffff00 solid; border-left: 10px #ffffff00 solid; - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); } + .to .typing_loader { animation: typing-black 1s linear infinite alternate; } @-webkit-keyframes typing { 0% { - background-color: rgba(255,255,255, 1); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,0.2); + background-color: rgba(255, 255, 255, 1); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 0.2); } 50% { - background-color: rgba(255,255,255, 0.4); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,1), 24px 0px 0px 0px rgba(255,255,255,0.4); + background-color: rgba(255, 255, 255, 0.4); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 1), 24px 0px 0px 0px rgba(255, 255, 255, 0.4); } 100% { - background-color: rgba(255,255,255, 0.2); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,1); + background-color: rgba(255, 255, 255, 0.2); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 1); } } @-moz-keyframes typing { 0% { - background-color: rgba(255,255,255, 1); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,0.2); + background-color: rgba(255, 255, 255, 1); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 0.2); } 50% { - background-color: rgba(255,255,255, 0.4); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,1), 24px 0px 0px 0px rgba(255,255,255,0.4); + background-color: rgba(255, 255, 255, 0.4); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 1), 24px 0px 0px 0px rgba(255, 255, 255, 0.4); } 100% { - background-color: rgba(255,255,255, 0.2); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,1); + background-color: rgba(255, 255, 255, 0.2); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 1); } } @@ -75,29 +78,29 @@ pre::-webkit-scrollbar-thumb{ 50% { background-color: rgba(74, 74, 74, 0.4); - box-shadow: 12px 0px 0px 0px rgba(74, 74, 74, 1), 24px 0px 0px 0px rgba(74, 74, 74,0.4); + box-shadow: 12px 0px 0px 0px rgba(74, 74, 74, 1), 24px 0px 0px 0px rgba(74, 74, 74, 0.4); } 100% { background-color: rgba(74, 74, 74, 0.2); - box-shadow: 12px 0px 0px 0px rgba(74, 74, 74,0.4), 24px 0px 0px 0px rgba(74, 74, 74,1); + box-shadow: 12px 0px 0px 0px rgba(74, 74, 74, 0.4), 24px 0px 0px 0px rgba(74, 74, 74, 1); } } @keyframes typing { 0% { - background-color: rgba(255,255,255, 1); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,0.2); + background-color: rgba(255, 255, 255, 1); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 0.2); } 50% { - background-color: rgba(255,255,255, 0.4); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,1), 24px 0px 0px 0px rgba(255,255,255,0.4); + background-color: rgba(255, 255, 255, 0.4); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 1), 24px 0px 0px 0px rgba(255, 255, 255, 0.4); } 100% { - background-color: rgba(255,255,255, 0.2); - box-shadow: 12px 0px 0px 0px rgba(255,255,255,0.4), 24px 0px 0px 0px rgba(255,255,255,1); + background-color: rgba(255, 255, 255, 0.2); + box-shadow: 12px 0px 0px 0px rgba(255, 255, 255, 0.4), 24px 0px 0px 0px rgba(255, 255, 255, 1); } } @@ -112,27 +115,30 @@ pre::-webkit-scrollbar-thumb{ .convFormDynamic textarea.userInputDynamic { border: none; padding: 7px 10px; - overflow-x: hidden!important; + overflow-x: hidden !important; outline: none; font-size: 0.905rem; float: left; width: calc(100% - 70px); line-height: 1.3em; - min-height: 1.7em; + min-height: 2em; max-height: 10rem; display: block; max-width: 89vw; margin-right: -1vw; resize: none; } -.convFormDynamic textarea::-webkit-scrollbar{ + +.convFormDynamic textarea::-webkit-scrollbar { width: 2px; background-color: lawngreen; } -.convFormDynamic textarea::-webkit-scrollbar-thumb{ - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); - background-color: dodgerblue; + +.convFormDynamic textarea::-webkit-scrollbar-thumb { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); + background-color: dodgerblue; } + .convFormDynamic input.userInputDynamic { border: none; padding: 7px 10px; @@ -180,16 +186,20 @@ div.conv-form-wrapper:before { top: 0; left: 0; z-index: 2; - background: linear-gradient(#fff, transparent); + background: linear-gradient(#ffffff3b, transparent); } @media (max-width: 767px) { - div.conv-form-wrapper div.wrapper-messages, div.conv-form-wrapper div#messages { + + div.conv-form-wrapper div.wrapper-messages, + div.conv-form-wrapper div#messages { max-height: 71vh; } } -div.conv-form-wrapper div.wrapper-messages::-webkit-scrollbar, div#feed ul::-webkit-scrollbar, div.conv-form-wrapper div.options::-webkit-scrollbar { +div.conv-form-wrapper div.wrapper-messages::-webkit-scrollbar, +div#feed ul::-webkit-scrollbar, +div.conv-form-wrapper div.options::-webkit-scrollbar { width: 0px; height: 0px; /* remove scrollbar space */ @@ -261,12 +271,13 @@ div.conv-form-wrapper div#messages div.message.to { } div.conv-form-wrapper div#messages div.message.from { - background: dodgerblue; + background: dodgerblue; color: #fff; border-top-right-radius: 0; } -.message.to+.message.from, .message.from+.message.to { +.message.to+.message.from, +.message.from+.message.to { margin-top: 15px; } @@ -294,7 +305,7 @@ div.conv-form-wrapper div#messages div.message.from { position: absolute; bottom: 0px; border: none; - left:95%; + left: 95%; margin: 5px; color: #fff; cursor: pointer; @@ -315,10 +326,11 @@ div.conv-form-wrapper div#messages div.message.from { } button.submit.glow { - border: 1px solid dodgerblue !important; - background: dodgerblue !important; - box-shadow: 0 0 5px 2px rgba(14, 144, 255,0.4); + border: 1px solid dodgerblue !important; + background: dodgerblue !important; + box-shadow: 0 0 5px 2px rgba(14, 144, 255, 0.4); } + .no-border { border: none !important; } @@ -327,7 +339,8 @@ button.submit.glow { cursor: grab; } -div.conv-form-wrapper div#messages::-webkit-scrollbar, div#feed ul::-webkit-scrollbar { +div.conv-form-wrapper div#messages::-webkit-scrollbar, +div#feed ul::-webkit-scrollbar { width: 0px; /* remove scrollbar space */ background: transparent; @@ -338,3 +351,268 @@ span.clear { display: block; clear: both; } + +.drawer-icon-container { + position: fixed; + top: calc(50% - 24px); + right: -30px; + z-index: 1000; + transition: right 0.5s ease; +} + +.drawer-icon { + width: 30px; + height: 30px; + cursor: pointer; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + background-color: #b1cee350; + padding-left: 22px; + border-radius: 50%; +} +.drawer-icon:hover{ + background-color: #005eff96; +} +.wrenchFilled.icon { + margin-left: -13px; + margin-top: 5px; + width: 10px; + height: 10px; + border-radius: 50%; + background-color: #333333; + transform-origin: center 10.5px; + transform: rotate(-45deg); +} + +.wrenchFilled.icon:after { + width: 0; + height: 0; + border-radius: 0 0 1px 1px; + background-color: #333333; + border-left: solid 1px transparent; + border-right: solid 1px transparent; + border-top: solid 1px white; + border-bottom: solid 1px transparent; + left: 4px; + top: 4px; +} + +.wrenchFilled.icon:before { + width: 2px; + height: 5px; + background-color: white; + left: 4px; + border-radius: 0 0 1px 1px; + box-shadow: 0 15px 0px 1px #333333, 0 11px 0px 1px #333333, 0 8px 0px 1px #333333; +} + +.icon { + position: absolute; +} + +.icon:before, +.icon:after { + content: ''; + position: absolute; + display: block; +} + +.icon i { + position: absolute; +} + +.icon i:before, +.icon i:after { + content: ''; + position: absolute; + display: block; +} + +.drawer-icon i { + margin-left: -15px; + line-height: 30px; + font-weight: bolder; +} + +.drawer { + position: fixed; + top: 0; + right: -300px; + width: 300px; + height: 100%; + background-color: #fff; + z-index: 999; + transition: right 0.5s ease; + display: flex; + flex-direction: column; +} + +.drawer.open { + right: 0; +} + +.drawer-header { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #b1cee350; + border-bottom: 1px solid #ddd; + padding: 16px; +} + +.drawer-header h2 { + margin: 0 0 0 16px; +} + +.drawer-header button { + background-color: transparent; + border: none; + cursor: pointer; +} + +.drawer-content { + flex: 1 1 auto; + height: 100%; + overflow: auto; + padding: 16px; +} + +.drawer-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 998; + display: none; +} + +@-webkit-keyframes click-wave { + 0% { + width: 40px; + height: 40px; + opacity: 0.35; + position: relative; + } + + 100% { + width: 60px; + height: 60px; + margin-left: 80px; + margin-top: 80px; + opacity: 0.0; + } +} + +@-moz-keyframes click-wave { + 0% { + width: 30px; + height: 30px; + opacity: 0.35; + position: relative; + } + + 100% { + width: 80px; + height: 80px; + margin-left: -23px; + margin-top: -23px; + opacity: 0.0; + } +} + +@-o-keyframes click-wave { + 0% { + width: 30px; + height: 30px; + opacity: 0.35; + position: relative; + } + + 100% { + width: 80px; + height: 80px; + margin-left: -23px; + margin-top: -23px; + opacity: 0.0; + } +} + +@keyframes click-wave { + 0% { + width: 30px; + height: 30px; + opacity: 0.35; + position: relative; + } + + 100% { + width: 80px; + height: 80px; + margin-left: -23px; + margin-top: -23px; + opacity: 0.0; + } +} + +.option-input { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + appearance: none; + position: relative; + top: 10px; + width: 30px; + height: 30px; + -webkit-transition: all 0.15s ease-out 0; + -moz-transition: all 0.15s ease-out 0; + transition: all 0.15s ease-out 0; + background: #cbd1d8; + border: none; + color: #fff; + cursor: pointer; + display: inline-block; + outline: none; + position: relative; + margin-right: 0.5rem; + z-index: 1000; +} + +.option-input:hover { + background: #9faab7; +} + +.option-input:checked { + background: #1e90ffaa; +} + +.option-input:checked::before { + width: 30px; + height: 30px; + position: absolute; + content: '☻'; + display: inline-block; + font-size: 29px; + text-align: center; + line-height: 26px; +} + +.option-input:checked::after { + -webkit-animation: click-wave 0.65s; + -moz-animation: click-wave 0.65s; + animation: click-wave 0.65s; + background: #40e0d0; + content: ''; + display: block; + position: relative; + z-index: 100; +} + +.option-input.radio { + border-radius: 50%; +} + +.option-input.radio::after { + border-radius: 50%; +} \ No newline at end of file diff --git a/channel/http/static/1.js b/channel/http/static/1.js index 4a34289..991f918 100644 --- a/channel/http/static/1.js +++ b/channel/http/static/1.js @@ -1,20 +1,29 @@ -function ConvState(wrapper, form, params) { - this.id='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { +function generateUUID () { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); - }); + }) +} + +const conversationType = { + DISPOSABLE: 1, + STREAM: 1 << 1 +} +function ConvState (wrapper, form, params) { + this.id = generateUUID() this.form = form; this.wrapper = wrapper; + this.backgroundColor = '#ffffff'; this.parameters = params; this.scrollDown = function () { $(this.wrapper).find('#messages').stop().animate({ scrollTop: $(this.wrapper).find('#messages')[0].scrollHeight }, 600); }.bind(this); }; -ConvState.prototype.printAnswer = function (answer = '我是ChatGPT, 一个由OpenAI训练的大型语言模型, 我旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。') { +ConvState.prototype.printAnswer = function (uuid, answer = '我是ChatGPT, 一个由OpenAI训练的大型语言模型, 我旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。') { setTimeout(function () { - var messageObj = $(this.wrapper).find('.message.typing'); + var messageObj = $(this.wrapper).find(`#${uuid}`); answer = marked.parse(answer); messageObj.html(answer); messageObj.removeClass('typing').addClass('ready'); @@ -22,39 +31,66 @@ ConvState.prototype.printAnswer = function (answer = '我是ChatGPT, 一个由Op $(this.wrapper).find(this.parameters.inputIdHashTagName).focus(); }.bind(this), 500); }; +ConvState.prototype.updateAnswer = function (question, uuid) { + setTimeout(function () { + var socket = io('/chat'); + socket.connect('/chat'); + var messageObj = $(this.wrapper).find(`#${uuid}`); + this.scrollDown(); + socket.on('message', msg => { + console.log("message:", msg) + if (msg.result) + messageObj.html(msg.result + `
`); + }); + socket.on('connect', msg => { + socket.emit('message', { data: JSON.stringify(question) }); + }); + socket.on('disconnect', msg => { + if (msg.result) { + answer = marked.parse(msg.result); + messageObj.html(answer); + } + messageObj.removeClass('typing').addClass('ready'); + this.scrollDown(); + $(this.wrapper).find(this.parameters.inputIdHashTagName).focus(); + console.log("disconnect", msg) + }); + }.bind(this), 1000); +}; ConvState.prototype.sendMessage = function (msg) { var message = $(''); - $('button.submit').removeClass('glow'); $(this.wrapper).find(this.parameters.inputIdHashTagName).focus(); setTimeout(function () { $(this.wrapper).find("#messages").append(message); this.scrollDown(); }.bind(this), 100); - - var messageObj = $(''); + var uuid = generateUUID().toLowerCase(); + var messageObj = $(``); setTimeout(function () { $(this.wrapper).find('#messages').append(messageObj); this.scrollDown(); }.bind(this), 150); var _this = this - $.ajax({ - url: "./chat", - type: "POST", - timeout:180000, - data: JSON.stringify({ - "id": _this.id, - "msg": msg - }), - contentType: "application/json; charset=utf-8", - dataType: "json", - success: function (data) { - _this.printAnswer(data.result) - }, - error:function () { - _this.printAnswer("网络故障,对话未送达") - }, - }) + var question = { "id": _this.id, "msg": msg } + if (localConfig.conversationType == conversationType.STREAM) + this.updateAnswer(question, uuid) + else + $.ajax({ + url: "./chat", + type: "POST", + timeout: 180000, + data: JSON.stringify(question), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: function (data) { + _this.printAnswer(uuid, data.result) + }, + error: function (data) { + console.log(data) + _this.printAnswer(uuid, "网络故障,对话未送达") + }, + }) }; (function ($) { $.fn.convform = function () { @@ -81,13 +117,30 @@ ConvState.prototype.sendMessage = function (msg) { $(wrapper).append(inputForm); var state = new ConvState(wrapper, form, parameters); + // Bind checkbox values to ConvState object + $('input[type="checkbox"]').change(function () { + var key = $(this).attr('name'); + state[key] = $(this).is(':checked'); + }); + + // Bind radio button values to ConvState object + $('input[type="radio"]').change(function () { + var key = $(this).attr('name'); + state[key] = $(this).val(); + }); + + // Bind color input value to ConvState object + $('#backgroundColor').change(function () { + state["backgroundColor"] = $(this).val(); + }); //prints first contact $.when($('div.spinLoader').addClass('hidden')).done(function () { - var messageObj = $(''); + var uuid = generateUUID() + var messageObj = $(``); $(state.wrapper).find('#messages').append(messageObj); state.scrollDown(); - state.printAnswer(); + state.printAnswer(uuid = uuid); }); //binds enter to send message diff --git a/channel/http/templates/index.html b/channel/http/templates/index.html index f05311f..798965b 100644 --- a/channel/http/templates/index.html +++ b/channel/http/templates/index.html @@ -19,33 +19,137 @@