设计一个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

 


本页地址:http://www.foxtable.com/webhelp/topics/7210.htm