feat: 添加文字输入对话功能

- 后端 WsQuery 添加 text 字段,支持文本输入模式
- Pipeline 支持文本查询时跳过 STT 直接使用输入文本
- 前端 ChatPanel 添加文字输入框,连接状态下可用
- useVisionSession 添加 sendTextMessage 方法
- 更新接口文档,添加文本输入模式说明
This commit is contained in:
2026-06-14 12:52:39 +08:00
parent 563ca12790
commit 51ed6aa563
8 changed files with 252 additions and 73 deletions

View File

@@ -1,9 +1,9 @@
// ============================================================
// ChatPanel — 消息展示面板
// 职责:渲染对话消息列表、流式光标、元数据、自动滚动
// 职责:渲染对话消息列表、流式光标、元数据、自动滚动、文本输入
// ============================================================
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import type { ChatMessage } from "../../types";
import type { ConnectionStatus } from "../../lib/websocket";
@@ -11,12 +11,16 @@ interface ChatPanelProps {
messages: ChatMessage[];
currentReply?: string;
connectionStatus: ConnectionStatus;
onSendText?: (text: string) => void;
}
export function ChatPanel({ messages, currentReply, connectionStatus }: ChatPanelProps) {
export function ChatPanel({ messages, currentReply, connectionStatus, onSendText }: ChatPanelProps) {
const bottomRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const isAutoScroll = useRef(true);
const [inputText, setInputText] = useState("");
const isConnected = connectionStatus === "connected";
// 用户上滚时暂停自动滚动,滚到底部时恢复
useEffect(() => {
@@ -39,9 +43,17 @@ export function ChatPanel({ messages, currentReply, connectionStatus }: ChatPane
}
}, [messages, currentReply]);
// 提交文本消息
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!inputText.trim() || !onSendText) return;
onSendText(inputText);
setInputText("");
};
// 空状态
if (messages.length === 0 && !currentReply) {
if (connectionStatus !== "connected") {
if (!isConnected) {
return (
<div className="chat-panel chat-panel--empty">
<span className="chat-panel--empty-icon">💬</span>
@@ -60,35 +72,58 @@ export function ChatPanel({ messages, currentReply, connectionStatus }: ChatPane
}
return (
<div className="chat-panel" ref={containerRef}>
{messages.map((msg, index) => (
<div key={index} className={`chat-message chat-message--${msg.role}`}>
<div className="chat-message__role">
{msg.role === "user" ? "你" : "AI"}
</div>
<div className="chat-message__content">{msg.content}</div>
{msg.role === "assistant" && msg.tokensUsed !== undefined && (
<div className="chat-message__meta">
{msg.tokensUsed} tokens
{msg.latencyMs !== undefined && ` · ${(msg.latencyMs / 1000).toFixed(1)}s`}
{msg.model && ` · ${msg.model}`}
<div className="chat-panel">
<div className="chat-panel__messages" ref={containerRef}>
{messages.map((msg, index) => (
<div key={index} className={`chat-message chat-message--${msg.role}`}>
<div className="chat-message__role">
{msg.role === "user" ? "你" : "AI"}
</div>
)}
</div>
))}
{/* 流式回复(尚未完成) */}
{currentReply && (
<div className="chat-message chat-message--assistant chat-message--streaming">
<div className="chat-message__role">AI</div>
<div className="chat-message__content">
{currentReply}
<span className="cursor"></span>
<div className="chat-message__content">{msg.content}</div>
{msg.role === "assistant" && msg.tokensUsed !== undefined && (
<div className="chat-message__meta">
{msg.tokensUsed} tokens
{msg.latencyMs !== undefined && ` · ${(msg.latencyMs / 1000).toFixed(1)}s`}
{msg.model && ` · ${msg.model}`}
</div>
)}
</div>
</div>
)}
))}
<div ref={bottomRef} />
{/* 流式回复(尚未完成) */}
{currentReply && (
<div className="chat-message chat-message--assistant chat-message--streaming">
<div className="chat-message__role">AI</div>
<div className="chat-message__content">
{currentReply}
<span className="cursor"></span>
</div>
</div>
)}
<div ref={bottomRef} />
</div>
{/* 文本输入框 */}
{isConnected && onSendText && (
<form className="chat-input" onSubmit={handleSubmit}>
<input
type="text"
className="chat-input__field"
placeholder="输入文字对话..."
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
<button
type="submit"
className="chat-input__send"
disabled={!inputText.trim()}
title="发送"
>
</button>
</form>
)}
</div>
);
}