feat: 手势开启新对话 —— 确认弹窗卡片 + 新对话首条用默认欢迎语
- 对话中举手 → 弹出确认卡片(再次举手或点击「确认」开启新对话,10s 倒计时自动取消;取消按钮可手动关闭) - 欢迎页(未开始对话)举手不响应 - 开启新对话后第一条消息 = 默认完整欢迎语(「问问小园」问候),不再只发「已开启新的对话」 - 1 分钟无新消息自动清空本轮,回到默认欢迎语(原 2 分钟) - 新增 hand-stop/camera 图标与确认卡片样式
This commit is contained in:
Generated
-54
@@ -1148,9 +1148,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1164,9 +1161,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1180,9 +1174,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1196,9 +1187,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1212,9 +1200,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1228,9 +1213,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1244,9 +1226,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1260,9 +1239,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1276,9 +1252,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1292,9 +1265,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1308,9 +1278,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1324,9 +1291,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1340,9 +1304,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1783,9 +1744,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1803,9 +1761,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1823,9 +1778,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1843,9 +1795,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1863,9 +1812,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
+108
-44
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import Icon from './Icons';
|
||||
import { API_BASE } from '../config';
|
||||
import { getMqttStatus } from '../utils/mqtt';
|
||||
import { renderMarkdown } from '../voice/markdown.jsx';
|
||||
|
||||
/* =========================================================
|
||||
AI 智能助手对话面板 —— 文字 / 语音双模式
|
||||
@@ -30,6 +31,9 @@ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
/* 本地兜底识别题库(ASR 不可用时) */
|
||||
const VOICE_SAMPLES = QUICK_QUESTIONS;
|
||||
|
||||
/* 默认欢迎语(新对话第一条消息,与初始一致) */
|
||||
const WELCOME_TEXT = '您好,我是「问问小园」,昆明市大学生创业园的专属 AI 助手(通义千问)。\n\n可以为您解答入驻申请、创业政策、创业担保贷款、OPC 概念、AI 工具等问题,也可以介绍园区入驻企业。试试右侧的预设问题吧。';
|
||||
|
||||
/* ---------- 消息 ---------- */
|
||||
let msgId = 0;
|
||||
const nextId = () => ++msgId;
|
||||
@@ -50,16 +54,22 @@ function executeLocalTools(tools, mqttPublished) {
|
||||
}
|
||||
|
||||
export default function AiChatPanel() {
|
||||
const [msgs, setMsgs] = useState(() => [{ id: nextId(), role: 'ai', text: '您好,我是 OPC 园区 AI 智能助手(阿里云通义千问)。\n\n支持文字与语音提问,可以为您解答入驻流程、政策扶持、场地服务、AI 赋能等问题,还可以通过语音或文字让我切换页面、展示企业卡片。' }]);
|
||||
const [msgs, setMsgs] = useState(() => [{ id: nextId(), role: 'ai', text: WELCOME_TEXT }]);
|
||||
const [input, setInput] = useState('');
|
||||
const [typing, setTyping] = useState(false);
|
||||
const [mode, setMode] = useState('text');
|
||||
const [voiceState, setVoiceState] = useState('idle'); // idle | recording | transcribing
|
||||
const [newTalkAsk, setNewTalkAsk] = useState(false); // 手势确认开启新对话中
|
||||
const [confirmLeft, setConfirmLeft] = useState(0); // 确认倒计时(秒)
|
||||
const scrollRef = useRef(null);
|
||||
const timerRef = useRef(null);
|
||||
const idleTimerRef = useRef(null); // 1 分钟空闲自动清空
|
||||
const confirmTimerRef = useRef(null); // 手势确认 10s 自动取消
|
||||
const recorderRef = useRef(null);
|
||||
const chunksRef = useRef([]);
|
||||
const voiceStateRef = useRef(voiceState);
|
||||
const msgsRef = useRef(msgs);
|
||||
useEffect(() => { msgsRef.current = msgs; }, [msgs]);
|
||||
|
||||
useEffect(() => { voiceStateRef.current = voiceState; }, [voiceState]);
|
||||
|
||||
@@ -77,7 +87,22 @@ export default function AiChatPanel() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** 发送消息 → 后端 LLM */
|
||||
/** 1 分钟无新消息 → 自动清空本轮,回到默认欢迎语 */
|
||||
const resetIdleTimer = useCallback(() => {
|
||||
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
||||
idleTimerRef.current = setTimeout(() => {
|
||||
setMsgs([{ id: nextId(), role: 'ai', text: WELCOME_TEXT }]);
|
||||
setTyping(false);
|
||||
setNewTalkAsk(false);
|
||||
}, 60000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
resetIdleTimer();
|
||||
return () => { if (idleTimerRef.current) clearTimeout(idleTimerRef.current); };
|
||||
}, [resetIdleTimer]);
|
||||
|
||||
/** 发送消息 → 后端 LLM(多轮:携带历史上下文) */
|
||||
const send = useCallback(async (raw, viaVoice = false) => {
|
||||
const text = String(raw || '').trim();
|
||||
if (!text || typing) return;
|
||||
@@ -88,10 +113,15 @@ export default function AiChatPanel() {
|
||||
let reply = null;
|
||||
let tools = [];
|
||||
try {
|
||||
// 多轮:将历史对话(去掉系统首条问候)作为 messages 上下文
|
||||
const history = msgsRef.current
|
||||
.filter((m) => m.role === 'me' || (m.role === 'ai' && !/我是「问问小园」/.test(m.text)))
|
||||
.map((m) => ({ role: m.role === 'me' ? 'user' : 'assistant', content: m.text }));
|
||||
history.push({ role: 'user', content: text });
|
||||
const res = await fetch(`${API_BASE}/api/ai/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ messages: [{ role: 'user', content: text }] }),
|
||||
body: JSON.stringify({ messages: history }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
@@ -104,13 +134,19 @@ export default function AiChatPanel() {
|
||||
setTyping(false);
|
||||
setMsgs((prev) => [...prev, { id: nextId(), role: 'ai', text: reply }]);
|
||||
executeLocalTools(tools);
|
||||
}, [typing]);
|
||||
resetIdleTimer(); // 有提问 → 重置 1 分钟空闲计时
|
||||
}, [typing, resetIdleTimer]);
|
||||
|
||||
/** 清空对话:新对话第一条消息与默认一致(欢迎语) */
|
||||
const clear = useCallback(() => {
|
||||
clearTimeout(timerRef.current);
|
||||
setTyping(false);
|
||||
setMsgs([{ id: nextId(), role: 'ai', text: '您好,我是 OPC 园区 AI 智能助手。有什么可以帮您?' }]);
|
||||
}, []);
|
||||
setNewTalkAsk(false);
|
||||
setConfirmLeft(0);
|
||||
if (confirmTimerRef.current) { clearTimeout(confirmTimerRef.current); confirmTimerRef.current = null; }
|
||||
setMsgs([{ id: nextId(), role: 'ai', text: WELCOME_TEXT }]);
|
||||
resetIdleTimer();
|
||||
}, [resetIdleTimer]);
|
||||
|
||||
/* ================= 语音对话(录音 → 后端 ASR) ================= */
|
||||
|
||||
@@ -200,6 +236,54 @@ export default function AiChatPanel() {
|
||||
return () => window.removeEventListener('dpm:ai-action', onAiAction);
|
||||
}, [send]);
|
||||
|
||||
// 手势控制:对话中举手 → 弹出确认卡片;再次举手或点击确认 → 开启新对话;10s 超时取消
|
||||
const confirmNewTalk = useCallback(() => {
|
||||
setNewTalkAsk(false);
|
||||
setConfirmLeft(0);
|
||||
if (confirmTimerRef.current) { clearTimeout(confirmTimerRef.current); confirmTimerRef.current = null; }
|
||||
clear();
|
||||
}, [clear]);
|
||||
|
||||
const cancelNewTalk = useCallback(() => {
|
||||
setNewTalkAsk(false);
|
||||
setConfirmLeft(0);
|
||||
if (confirmTimerRef.current) { clearTimeout(confirmTimerRef.current); confirmTimerRef.current = null; }
|
||||
setMsgs((prev) => [...prev, { id: nextId(), role: 'ai', text: '已取消开启新对话。' }]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onGesture = (e) => {
|
||||
if (e.detail?.type !== 'raise') return;
|
||||
if (newTalkAsk) {
|
||||
// 再次举手:确认开启新对话
|
||||
confirmNewTalk();
|
||||
} else {
|
||||
// 仅对话中响应(默认欢迎语不算对话)
|
||||
if (msgsRef.current.length <= 1) return;
|
||||
setNewTalkAsk(true);
|
||||
setConfirmLeft(10);
|
||||
if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current);
|
||||
confirmTimerRef.current = setTimeout(() => {
|
||||
setNewTalkAsk(false);
|
||||
setConfirmLeft(0);
|
||||
setMsgs((prev) => [...prev, { id: nextId(), role: 'ai', text: '已取消开启新对话。' }]);
|
||||
}, 10000);
|
||||
}
|
||||
};
|
||||
window.addEventListener('dpm:gesture', onGesture);
|
||||
return () => {
|
||||
window.removeEventListener('dpm:gesture', onGesture);
|
||||
if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current);
|
||||
};
|
||||
}, [newTalkAsk, confirmNewTalk]);
|
||||
|
||||
// 确认卡片倒计时显示
|
||||
useEffect(() => {
|
||||
if (!newTalkAsk) { setConfirmLeft(0); return; }
|
||||
const iv = setInterval(() => setConfirmLeft((p) => Math.max(0, p - 1)), 1000);
|
||||
return () => clearInterval(iv);
|
||||
}, [newTalkAsk]);
|
||||
|
||||
const toggleVoice = useCallback(() => {
|
||||
if (voiceStateRef.current === 'recording') {
|
||||
const rec = recorderRef.current;
|
||||
@@ -233,16 +317,8 @@ export default function AiChatPanel() {
|
||||
<div className="bd-chat-head">
|
||||
<span className="bd-chat-logo"><Icon name="robot" size={17} /></span>
|
||||
<div className="bd-chat-titles">
|
||||
<b>AI 智能助手</b>
|
||||
<span>阿里云通义千问 · 语音识别</span>
|
||||
</div>
|
||||
<div className="bd-chat-modes">
|
||||
<button className={mode === 'text' ? 'active' : ''} onClick={() => switchMode('text')} title="文字对话">
|
||||
<Icon name="message-circle" size={13} />文字
|
||||
</button>
|
||||
<button className={mode === 'voice' ? 'active' : ''} onClick={() => switchMode('voice')} title="语音对话">
|
||||
<Icon name="microphone" size={13} />语音
|
||||
</button>
|
||||
<b>问问小园</b>
|
||||
<span>昆明市大学生创业园 · 通义千问</span>
|
||||
</div>
|
||||
<span className="bd-chat-model"><i />通义千问</span>
|
||||
<button className="bd-chat-clear" title="清空对话" onClick={clear}>
|
||||
@@ -257,7 +333,7 @@ export default function AiChatPanel() {
|
||||
{m.role === 'ai' && <span className="bd-msg-avatar"><Icon name="robot" size={13} /></span>}
|
||||
<div className="bd-msg-bubble">
|
||||
{m.voice && <span className="bd-msg-voice-tag"><Icon name="microphone" size={10} />语音</span>}
|
||||
{m.text}
|
||||
{m.role === 'ai' ? renderMarkdown(m.text) : m.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -267,9 +343,22 @@ export default function AiChatPanel() {
|
||||
<div className="bd-msg-bubble bd-typing"><i /><i /><i /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 手势确认弹窗:检测到举手 → 是否开启新对话 */}
|
||||
{newTalkAsk && (
|
||||
<div className="bd-chat-confirm">
|
||||
<span className="bd-chat-confirm-ico"><Icon name="hand-stop" size={18} /></span>
|
||||
<div className="bd-chat-confirm-text">
|
||||
<b>检测到举手,是否开启新的对话?</b>
|
||||
<span>再次举手或点击「确认」开始新对话 · {confirmLeft}s 后自动取消</span>
|
||||
</div>
|
||||
<button className="bd-chat-confirm-btn" onClick={confirmNewTalk}>确认</button>
|
||||
<button className="bd-chat-confirm-btn ghost" onClick={cancelNewTalk}>取消</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 文字模式 */}
|
||||
{/* 文字输入区(固定,无文字/语音切换) */}
|
||||
{mode === 'text' && (
|
||||
<>
|
||||
<div className="bd-chat-quick">
|
||||
@@ -283,7 +372,7 @@ export default function AiChatPanel() {
|
||||
<div className="bd-chat-input">
|
||||
<input
|
||||
value={input}
|
||||
placeholder="输入你的问题,如:展示入驻企业卡片"
|
||||
placeholder="输入你的问题,如:什么是 OPC?"
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
|
||||
@@ -298,31 +387,6 @@ export default function AiChatPanel() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 语音模式 */}
|
||||
{mode === 'voice' && (
|
||||
<div className="bd-chat-voice">
|
||||
<div className={`bd-chat-voice-hint ${voiceState}`}>
|
||||
{voiceState === 'recording' && <span className="bd-voice-wave"><i /><i /><i /><i /><i /></span>}
|
||||
{voiceHint}
|
||||
</div>
|
||||
<button
|
||||
className={`bd-chat-mic ${voiceState}`}
|
||||
onClick={toggleVoice}
|
||||
title={voiceState === 'recording' ? '结束并发送' : '开始语音提问'}
|
||||
>
|
||||
{voiceState === 'recording' && <span className="bd-mic-ring r1" />}
|
||||
{voiceState === 'recording' && <span className="bd-mic-ring r2" />}
|
||||
<Icon
|
||||
name={voiceState === 'recording' ? 'microphone-filled' : 'microphone'}
|
||||
size={30}
|
||||
/>
|
||||
</button>
|
||||
<div className="bd-chat-voice-tip">
|
||||
{voiceState === 'recording' ? '再次点击结束并识别' : '点击开始说话 · 阿里云语音识别'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Volume, VolumeOff, ChevronLeft, ChevronRight, PlayerPlay, PlayerPause,
|
||||
Send, Trash, Eraser, Headset, CircleDot, AlertCircle,
|
||||
Microphone, MicrophoneFilled, MicrophoneOff, MessageCircle,
|
||||
Camera, CameraOff, HandStop,
|
||||
} from '@appica/icons-react';
|
||||
|
||||
/* 名称 → 图标组件映射(推荐的调用方式:icon="bolt") */
|
||||
@@ -85,6 +86,9 @@ const NAME_MAP = {
|
||||
'microphone-filled': MicrophoneFilled,
|
||||
'microphone-off': MicrophoneOff,
|
||||
'message-circle': MessageCircle,
|
||||
camera: Camera,
|
||||
'camera-off': CameraOff,
|
||||
'hand-stop': HandStop,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -119,4 +123,5 @@ export {
|
||||
Volume, VolumeOff, ChevronLeft, ChevronRight, PlayerPlay, PlayerPause,
|
||||
Send, Trash, Eraser, Headset, CircleDot, AlertCircle,
|
||||
Microphone, MicrophoneFilled, MicrophoneOff, MessageCircle,
|
||||
Camera, CameraOff, HandStop,
|
||||
};
|
||||
|
||||
@@ -3222,3 +3222,223 @@
|
||||
.bd-twin-detail-head {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* ============ 预设问题面板(AI 助手右侧) ============ */
|
||||
.bd-prompt-side {
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
border-left: 1px solid rgba(47, 107, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.bd-prompt-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(47, 107, 255, 0.1), rgba(76, 194, 255, 0.08));
|
||||
border: 1px solid rgba(47, 107, 255, 0.14);
|
||||
}
|
||||
.bd-prompt-head .appica-ico { color: #2f6bff; flex-shrink: 0; }
|
||||
.bd-prompt-head b { font-size: 14px; color: var(--bd-ink); }
|
||||
.bd-prompt-head span {
|
||||
margin-left: auto;
|
||||
font-size: 10.5px;
|
||||
color: var(--bd-sub);
|
||||
}
|
||||
.bd-prompt-group { display: flex; flex-direction: column; gap: 7px; }
|
||||
.bd-prompt-group-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--bd-sub);
|
||||
letter-spacing: 0.06em;
|
||||
border-left: 3px solid #2f6bff;
|
||||
padding-left: 8px;
|
||||
}
|
||||
.bd-prompt-list { display: flex; flex-direction: column; gap: 5px; }
|
||||
.bd-prompt-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 8px 11px;
|
||||
border: 1px solid rgba(47, 107, 255, 0.1);
|
||||
border-radius: 9px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
color: var(--bd-ink);
|
||||
font-size: 12.5px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.bd-prompt-item:hover {
|
||||
border-color: #2f6bff;
|
||||
color: #2f6bff;
|
||||
background: rgba(47, 107, 255, 0.06);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.bd-prompt-item .appica-ico { color: #f59e0b; flex-shrink: 0; margin-right: 2px; }
|
||||
.bd-prompt-tools { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.bd-prompt-tool {
|
||||
font-size: 11px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
color: #0e7490;
|
||||
background: rgba(34, 211, 238, 0.1);
|
||||
border: 1px solid rgba(34, 211, 238, 0.22);
|
||||
}
|
||||
|
||||
/* ============ Markdown 富文本渲染(问问小园回复) ============ */
|
||||
.md-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.7;
|
||||
color: var(--bd-ink);
|
||||
word-break: break-word;
|
||||
}
|
||||
.md-h1, .md-h2, .md-h3 {
|
||||
margin: 4px 0 0;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
color: var(--bd-ink);
|
||||
}
|
||||
.md-h1 { font-size: 15px; }
|
||||
.md-h2 { font-size: 13.5px; }
|
||||
.md-h3 { font-size: 12.5px; }
|
||||
.md-p { margin: 0; }
|
||||
.md-li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
.md-dot {
|
||||
flex-shrink: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-top: 7px;
|
||||
background: #2f6bff;
|
||||
}
|
||||
.md-oli { color: var(--bd-ink); }
|
||||
.md-quote {
|
||||
padding: 6px 10px;
|
||||
border-left: 3px solid #4cc2ff;
|
||||
background: rgba(47, 107, 255, 0.05);
|
||||
border-radius: 0 8px 8px 0;
|
||||
color: #4a5b76;
|
||||
}
|
||||
.md-hr { border-top: 1px dashed rgba(47, 107, 255, 0.25); margin: 2px 0; }
|
||||
.md-pre {
|
||||
margin: 2px 0;
|
||||
padding: 8px 10px;
|
||||
background: #0f172a;
|
||||
color: #dbe7ff;
|
||||
border-radius: 8px;
|
||||
font-size: 11.5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.md-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 2px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.md-table th, .md-table td {
|
||||
border: 1px solid rgba(47, 107, 255, 0.15);
|
||||
padding: 5px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
.md-table th {
|
||||
background: rgba(47, 107, 255, 0.08);
|
||||
font-weight: 700;
|
||||
color: var(--bd-ink);
|
||||
}
|
||||
.md-table tr:nth-child(even) td { background: rgba(244, 248, 253, 0.6); }
|
||||
.md-bold { font-weight: 700; color: var(--bd-ink); }
|
||||
.md-code {
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background: rgba(47, 107, 255, 0.08);
|
||||
color: #1d4fd8;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
/* ============ 手势确认弹窗(新对话确认卡片) ============ */
|
||||
.bd-chat-confirm {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 92%;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(47, 107, 255, 0.25);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 12px 34px rgba(47, 107, 255, 0.18);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
animation: bdOverlayIn 0.3s ease;
|
||||
}
|
||||
.bd-chat-confirm-ico {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: linear-gradient(140deg, var(--bd-blue), var(--bd-cyan));
|
||||
box-shadow: 0 5px 14px rgba(47, 107, 255, 0.32);
|
||||
}
|
||||
.bd-chat-confirm-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.bd-chat-confirm-text b {
|
||||
font-size: 13.5px;
|
||||
color: var(--bd-ink);
|
||||
}
|
||||
.bd-chat-confirm-text span {
|
||||
font-size: 11px;
|
||||
color: var(--bd-sub);
|
||||
}
|
||||
.bd-chat-confirm-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 7px 18px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
border-radius: 9px;
|
||||
color: #fff;
|
||||
background: linear-gradient(140deg, var(--bd-blue), var(--bd-blue-deep));
|
||||
box-shadow: 0 4px 12px rgba(47, 107, 255, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.bd-chat-confirm-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(47, 107, 255, 0.4);
|
||||
}
|
||||
.bd-chat-confirm-btn.ghost {
|
||||
color: var(--bd-sub);
|
||||
background: rgba(120, 160, 220, 0.1);
|
||||
border: 1px solid rgba(120, 160, 220, 0.2);
|
||||
box-shadow: none;
|
||||
}
|
||||
.bd-chat-confirm-btn.ghost:hover {
|
||||
color: var(--bd-red);
|
||||
background: rgba(240, 68, 56, 0.06);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user