设计一个AI对话框
本节内容参考CaseStudy目录下的文件"LLMCaller.Table"的窗口"AI对话框"。
本节的任务是开发一个支持多轮对话的流式AI对话框,也就是说这个对话框是支持上下文的,且有打字效果。
这个对话框是通过WebViewer对话框实现的,可能需要一点点网页设计知识,没有这方面的知识储备也没有关系,你可以通过AI(本节的网页代码是100%通过AI生成的),或直接套用本节的代码即可。
设计步骤
1、窗口的AfterLoad事件代码:
'''Async
' 1、初始化LLMCaller
Dim
caller
As
New
LLMCaller("https://api.deepseek.com/chat/completions",
"deepseek-flash",
"sk-a734b83536da45e9aedc3e42605bfb3a")
'请替换为自己的密钥
caller.stream =
True
'开启流式输出
caller.Messages =
New
JArray()
'开启多轮对话
caller.SystemPrompt =
"你是一个乐于助人的助手"
'设置系统提示词
Vars("caller")
= caller
'将初始化好的LLMCaller存放在Var变量中,方便复用
' 2.
初始化
WebViewer
Dim
wv
As
WebViewer = e.Form.Controls("WebViewer1").WebViewer
Await
wv.EnsureCoreWebView2Async(Nothing)
' 3.
注入
FoxLib(用于后续可能的扩展,如调用
Foxtable 功能)
Dim
ft
As
New
FoxLib(wv)
wv.CoreWebView2.AddHostObjectToScript("ft",
ft)
' 4. 加载聊天界面
HTML
Dim
html
As
String
=
<![CDATA[
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>DeepSeek
会话</title>
<script
src="https://cdn.jsdmirror.com/npm/marked/marked.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; font-family:
'Microsoft YaHei', sans-serif; }
body { background: #f5f7fa; padding: 20px; height: 100vh; display: flex;
flex-direction: column; }
#chatContainer { flex: 1; overflow-y: auto; background: #fff;
border-radius: 8px; padding: 15px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 15px; }
.message { margin: 10px 0; padding: 10px 14px; border-radius: 18px;
max-width: 80%; word-wrap: break-word; line-height: 1.6; clear: both; }
.message-user { background: #e1f5fe; float: right;
border-bottom-right-radius: 4px; color: #01579b; }
.message-assistant { background: #f0f0f0; float: left;
border-bottom-left-radius: 4px; color: #333; }
.message-error { background: #ffebee; float: left;
border-bottom-left-radius: 4px; color: #c62828; border: 1px solid #ef9a9a; }
/*
新增思考区块样式,区分背景
*/
.think-block {
background: #fff8e6;
border-radius: 12px;
padding: 8px 12px;
margin-bottom: 8px;
font-size: 14px;
color: #805b00;
}
.answer-block {
padding-top: 6px;
}
.message::after { content: ""; display: table; clear: both; }
.timestamp { font-size: 0.75em; color: #999; margin-top: 4px;
text-align: right; }
#emptyHint { color: #aaa; text-align: center; padding: 50px 0;
font-size: 16px; }
/* 新增:Markdown
渲染样式
*/
.answer-block table { border-collapse: collapse; margin: 8px 0; }
.answer-block th, .answer-block td { border: 1px solid #ccc; padding:
4px 10px; text-align: left; }
.answer-block th { background: #f0f0f0; }
.answer-block code { background: #f4f4f4; padding: 2px 6px;
border-radius: 4px; font-family: monospace; font-size: 0.9em; }
.answer-block pre { background: #f6f8fa; border: 1px solid #e1e4e8;
border-radius: 6px; padding: 16px; overflow-x: auto;
font-family: 'Consolas', 'Courier New', monospace; font-size: 14px;
line-height: 1.6; margin: 12px 0; }
.answer-block blockquote { border-left: 4px solid #ddd; padding-left:
14px; margin: 8px 0; color: #555; }
.answer-block ul, .answer-block ol { padding-left: 20px; margin: 6px 0;
}
.answer-block p { margin: 4px 0; }
</style>
</head>
<body>
<div id="chatContainer">
<div id="emptyHint">💬
输入问题开始对话...</div>
</div>
<script>
var msgDiv;
var thinkBox;
var answerBox;
let fullThink = "";
let fullAnswer = "";
//
添加消息到聊天区域,区分user/assistant两种渲染结构
function addMessage(role, content) {
console.log('content:'+ content);
var container = document.getElementById('chatContainer');
var hint = document.getElementById('emptyHint');
if (hint) hint.remove();
msgDiv = document.createElement('div');
msgDiv.className = 'message message-' + role;
//
用户消息:直接显示文本,不创建思考、回答分块
if(role === "user"){
var textNode = document.createElement('div');
textNode.innerHTML = content;
msgDiv.appendChild(textNode);
}
// AI助手消息:创建独立思考、回答两块容器
else if(role === "assistant"){
thinkBox = document.createElement('div');
thinkBox.className = 'think-block';
thinkBox.innerHTML = '<strong>思考过程:</strong><br>';
fullThink = "";
answerBox = document.createElement('div');
answerBox.className = 'answer-block';
answerBox.innerHTML = '<strong>回答:</strong><br>';
fullAnswer = "";
msgDiv.appendChild(thinkBox);
msgDiv.appendChild(answerBox);
}
//
时间戳
var time = new Date();
var timeStr = time.toLocaleTimeString('zh-CN', { hour: '2-digit',
minute: '2-digit' });
var ts = document.createElement('div');
ts.className = 'timestamp';
ts.textContent = timeStr;
msgDiv.appendChild(ts);
container.appendChild(msgDiv);
container.scrollTop = container.scrollHeight;
}
//追加思考内容
function appendThink(content) {
if(msgDiv){
fullThink += content;
thinkBox.innerHTML = '<strong>思考过程:</strong><br>'
+ fullThink;
var container = document.getElementById('chatContainer');
container.scrollTop = container.scrollHeight;
}
}
//追加回答内容
function appendAnswer(content) {
if(msgDiv){
fullAnswer += content;
//
唯一改动:使用
marked.parse 渲染
Markdown
answerBox.innerHTML = '<strong>回答:</strong><br>'
+ marked.parse(fullAnswer);
var container = document.getElementById('chatContainer');
container.scrollTop = container.scrollHeight;
}
}
function addTempMessage(content) {
var container = document.getElementById('chatContainer');
var hint = document.getElementById('emptyHint');
if (hint) hint.remove();
var msgDiv = document.createElement('div');
msgDiv.className = 'message message-assistant';
msgDiv.setAttribute('data-temp', 'true');
var textNode = document.createElement('div');
textNode.innerHTML = content;
msgDiv.appendChild(textNode);
container.appendChild(msgDiv);
container.scrollTop = container.scrollHeight;
}
function removeTempMessage() {
var temp = document.querySelector('.message[data-temp="true"]');
if (temp) temp.remove();
}
//
滚动到底部(可选)
function scrollToBottom() {
var container = document.getElementById('chatContainer');
if (container) container.scrollTop = container.scrollHeight;
}
</script>
</body>
</html>
]]>.Value
wv.NavigateToString(html)
2、发送按钮代码:
'''Async
Dim
wv
As
WebViewer = e.Form.Controls("WebViewer1").WebViewer
Dim
inputBox
As
WinForm.TextBox = e.Form.Controls("TextBox1")
Dim
userMsg
As
String
= inputBox.Text.Trim()
'获取用户输入的问题
If
userMsg =
""
Then
Return
inputBox.Text =
""
'清空输入框
' ---- 1、设置用户问题
----
Dim
caller
As
LLMCaller = Vars("caller")
'从Var变量中获取已经初始化的LLMCaller
caller.UserMessage = userMsg
'设置本轮用户问题
' ---- 2.
显示用户消息
----
Dim
userMsgEncoded
As
String
= HTMLEncode(userMsg).Replace(vbCrLf,
"<br>").Replace(vbLf,
"<br>")
Dim
userMsgJson
As
String
= JsonConvert.ToString(userMsgEncoded)
Await
wv.ExecuteScriptAsync("addMessage('user',
"
& userMsgJson &
")")
Await
wv.ExecuteScriptAsync("addTempMessage('⏳
正在思考中...')")
' ---- 3.
定义接收流式输出的回调函数,用于显示推理过程和答案
----
Dim
isFirst
As
Boolean
=
True
caller.SSEStreamHandler =
Async
Sub(ttype
As
String,
tcontent
As
String)
Dim
safeContent
As
String
= tcontent
If
ttype =
"think"
OrElse
ttype =
"error"
Then
safeContent = HtmlEncode(tcontent).Replace(vbCrLf,
"<br>").Replace(vbLf,
"<br>")
ElseIf
ttype =
"answer"
Then
'答案交给Markdown去处理即可
End
If
Dim
safeContentJson
As
String
= JsonConvert.ToString(safeContent)
If
isFirst
Then
isFirst =
False
Await
wv.ExecuteScriptAsync("removeTempMessage()")
Await
wv.ExecuteScriptAsync("addMessage('assistant',
'')")
End
If
If
ttype =
"think"
OrElse
ttype =
"error"
Then
Await
wv.ExecuteScriptAsync("appendThink("
& safeContentJson &
")")
ElseIf
ttype =
"answer"
Then
Await
wv.ExecuteScriptAsync("appendAnswer("
& safeContentJson &
")")
End
If
End
Sub
' ---- 4. 发送请求
----
e.Sender.Enabled =
False
Await
caller.GetResultAsync()
'请不要漏掉Await关键词
e.Sender.Enabled =
True
3、取消按钮代码:
If
Vars("caller")
IsNot
Nothing
Then
Dim
caller
As
LLMCaller = Vars("caller")
caller.Cancel()
End
If
4、新对话按钮代码:
If
Vars("caller")
IsNot
Nothing
Then
Dim
caller
As
LLMCaller = Vars("caller")
caller.Messages.Clear()
End
If