feat:添加对话情景功能
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
// ============================================================
|
||||
// ChatPanel — 消息展示面板
|
||||
// 职责:渲染对话消息列表、流式光标、元数据、自动滚动、文本输入
|
||||
// 增强:空状态场景卡片、语音输入按钮
|
||||
// 增强:空状态情景选择卡片、语音输入按钮
|
||||
// ============================================================
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useI18n } from "../../lib/i18n";
|
||||
import { scenarios } from "../../lib/scenarios";
|
||||
import type { ChatMessage } from "../../types";
|
||||
import type { ConnectionStatus } from "../../lib/websocket";
|
||||
|
||||
@@ -13,6 +14,7 @@ interface ChatPanelProps {
|
||||
messages: ChatMessage[];
|
||||
currentReply?: string;
|
||||
connectionStatus: ConnectionStatus;
|
||||
currentScenario?: string;
|
||||
isProcessing?: boolean;
|
||||
isVADReady?: boolean;
|
||||
vadError?: string;
|
||||
@@ -21,9 +23,10 @@ interface ChatPanelProps {
|
||||
onSendText?: (text: string) => void;
|
||||
onToggleMic?: () => void;
|
||||
onSceneCard?: (prompt: string) => void;
|
||||
onSelectScenario?: (scenarioId: string) => void;
|
||||
}
|
||||
|
||||
/** 场景卡片数据 */
|
||||
/** 场景卡片数据(视觉分析快捷) */
|
||||
function getSceneCards(t: (key: string) => string) {
|
||||
return [
|
||||
{ icon: "👁", titleKey: "scene.describe", descKey: "scene.describe.desc", prompt: t("scene.describe") },
|
||||
@@ -37,6 +40,7 @@ export function ChatPanel({
|
||||
messages,
|
||||
currentReply,
|
||||
connectionStatus,
|
||||
currentScenario,
|
||||
isProcessing,
|
||||
isVADReady,
|
||||
vadError,
|
||||
@@ -45,6 +49,7 @@ export function ChatPanel({
|
||||
onSendText,
|
||||
onToggleMic,
|
||||
onSceneCard,
|
||||
onSelectScenario,
|
||||
}: ChatPanelProps) {
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -55,6 +60,8 @@ export function ChatPanel({
|
||||
const isConnected = connectionStatus === "connected";
|
||||
const isEmpty = messages.length === 0 && !currentReply;
|
||||
const sceneCards = getSceneCards(t);
|
||||
const activeScenario = currentScenario || "free_chat";
|
||||
const isFreeChat = activeScenario === "free_chat";
|
||||
|
||||
// 用户上滚时暂停自动滚动,滚到底部时恢复
|
||||
useEffect(() => {
|
||||
@@ -97,46 +104,74 @@ export function ChatPanel({
|
||||
return (
|
||||
<div className="chat-panel">
|
||||
<div className="chat-panel__messages" ref={containerRef}>
|
||||
{/* 空状态:场景卡片 */}
|
||||
{/* 空状态:情景选择 + 场景卡片 */}
|
||||
{isEmpty && (
|
||||
<div className="chat-panel__welcome">
|
||||
<span className="chat-panel__welcome-icon">💬</span>
|
||||
<span className="chat-panel__welcome-icon">{scenarios.find(s => s.id === activeScenario)?.icon || "💬"}</span>
|
||||
<p>{isConnected ? t("chat.welcome.prompt") : t("chat.empty.prompt")}</p>
|
||||
<span className="chat-panel__welcome-hint">{isConnected ? t("chat.welcome.hint") : t("chat.empty.hint")}</span>
|
||||
|
||||
{/* 场景快捷卡片 */}
|
||||
<div className="scene-cards">
|
||||
{sceneCards.map((card) => (
|
||||
<button
|
||||
key={card.titleKey}
|
||||
className="scene-card"
|
||||
onClick={() => handleSceneCard(card.prompt)}
|
||||
>
|
||||
<span className="scene-card__icon">{card.icon}</span>
|
||||
<div className="scene-card__text">
|
||||
<span className="scene-card__title">{t(card.titleKey)}</span>
|
||||
<span className="scene-card__desc">{t(card.descKey)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* 情景选择卡片(非 free_chat 时隐藏,因为已通过 header 切换) */}
|
||||
{isFreeChat && onSelectScenario && (
|
||||
<div className="scenario-cards">
|
||||
<div className="scenario-cards__title">{t("scenario.choose")}</div>
|
||||
{scenarios.filter(s => s.id !== "free_chat").map((sc) => (
|
||||
<button
|
||||
key={sc.id}
|
||||
className="scenario-card"
|
||||
onClick={() => onSelectScenario(sc.id)}
|
||||
>
|
||||
<span className="scenario-card__icon">{sc.icon}</span>
|
||||
<div className="scenario-card__text">
|
||||
<span className="scenario-card__title">{t(sc.nameKey)}</span>
|
||||
<span className="scenario-card__desc">{t(sc.descKey)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 视觉分析快捷卡片(仅 free_chat 模式) */}
|
||||
{isFreeChat && isConnected && (
|
||||
<div className="scene-cards">
|
||||
{sceneCards.map((card) => (
|
||||
<button
|
||||
key={card.titleKey}
|
||||
className="scene-card"
|
||||
onClick={() => handleSceneCard(card.prompt)}
|
||||
>
|
||||
<span className="scene-card__icon">{card.icon}</span>
|
||||
<div className="scene-card__text">
|
||||
<span className="scene-card__title">{t(card.titleKey)}</span>
|
||||
<span className="scene-card__desc">{t(card.descKey)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((msg, index) => (
|
||||
<div key={index} className={`chat-message chat-message--${msg.role}`}>
|
||||
<div className="chat-message__role">
|
||||
{msg.role === "user" ? t("chat.userLabel") : "AI"}
|
||||
msg.role === "system" ? (
|
||||
<div key={index} className="chat-message chat-message--system">
|
||||
<span className="chat-message--system__text">{msg.content}</span>
|
||||
</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 key={index} className={`chat-message chat-message--${msg.role}`}>
|
||||
<div className="chat-message__role">
|
||||
{msg.role === "user" ? t("chat.userLabel") : "AI"}
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
|
||||
{/* 流式回复(尚未完成) */}
|
||||
|
||||
Reference in New Issue
Block a user