feat: 添加聊天功能及相关路由配置和UI组件,并引入 react-icons 依赖。
This commit is contained in:
402
src/pages/Chat/Chat.css
Normal file
402
src/pages/Chat/Chat.css
Normal file
@@ -0,0 +1,402 @@
|
||||
.chat-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.chat-header-back {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chat-header-back:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chat-header-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.chat-header-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-header-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chat-header-status {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Message List */
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.chat-message.user {
|
||||
flex-direction: row-reverse;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.chat-message.assistant {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.chat-message-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chat-message.assistant .chat-message-avatar {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
|
||||
.chat-message.user .chat-message-avatar {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-message-content {
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.chat-message.assistant .chat-message-content {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-message.user .chat-message-content {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
/* Confirmation Card */
|
||||
.confirmation-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.confirmation-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.confirmation-card-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.confirmation-card-fields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.confirmation-card-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.confirmation-card-field.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.confirmation-card-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.confirmation-card-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.confirmation-card-value.amount {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--color-expense);
|
||||
}
|
||||
|
||||
.confirmation-card-value.amount.income {
|
||||
color: var(--color-income);
|
||||
}
|
||||
|
||||
.confirmation-card-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.confirmation-card-btn {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.confirmation-card-btn.confirm {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirmation-card-btn.confirm:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.confirmation-card-btn.cancel {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.confirmation-card-btn.cancel:hover {
|
||||
background: var(--bg-quaternary);
|
||||
}
|
||||
|
||||
/* Input Area */
|
||||
.chat-input-area {
|
||||
padding: 12px 20px 24px;
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 24px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
resize: none;
|
||||
max-height: 120px;
|
||||
min-height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.chat-input::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.chat-input-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.chat-input-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chat-input-btn.send {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-input-btn.send:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.chat-input-btn.send:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chat-input-btn.recording {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.chat-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 16px;
|
||||
border-bottom-left-radius: 4px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.chat-loading-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chat-loading-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-tertiary);
|
||||
animation: bounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.chat-loading-dot:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.chat-loading-dot:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.chat-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.chat-empty-icon {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.chat-empty-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chat-empty-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.chat-empty-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chat-suggestion-chip {
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chat-suggestion-chip:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
377
src/pages/Chat/Chat.tsx
Normal file
377
src/pages/Chat/Chat.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FiArrowLeft, FiSend, FiMic, FiMicOff } from 'react-icons/fi';
|
||||
import aiService from '../../services/aiService';
|
||||
import type { AIChatResponse, ConfirmationCard } from '../../types';
|
||||
import './Chat.css';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
confirmationCard?: ConfirmationCard;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export default function Chat() {
|
||||
const navigate = useNavigate();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const audioChunksRef = useRef<Blob[]>([]);
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
// 发送消息
|
||||
const handleSend = async () => {
|
||||
if (!inputText.trim() || isLoading) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: inputText.trim(),
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInputText('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response: AIChatResponse = await aiService.sendChatMessage(
|
||||
userMessage.content,
|
||||
sessionId || undefined
|
||||
);
|
||||
|
||||
setSessionId(response.sessionId);
|
||||
|
||||
const assistantMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: response.message,
|
||||
confirmationCard: response.confirmationCard,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
const errorMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: '抱歉,我遇到了一些问题,请稍后再试。',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 确认记账
|
||||
const handleConfirm = async (card: ConfirmationCard) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await aiService.confirmTransaction(card);
|
||||
|
||||
const confirmMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'assistant',
|
||||
content: `✅ 已记录!${card.type === 'income' ? '收入' : '支出'} ${card.amount.toFixed(2)} 元\n分类:${card.category}\n账户:${card.account}`,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => {
|
||||
// 移除最后一条消息的确认卡片
|
||||
const updated = prev.map((msg, idx) => {
|
||||
if (idx === prev.length - 1 && msg.confirmationCard) {
|
||||
return { ...msg, confirmationCard: undefined };
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
return [...updated, confirmMessage];
|
||||
});
|
||||
|
||||
// 清除会话以开始新对话
|
||||
aiService.clearSession();
|
||||
setSessionId(null);
|
||||
} catch (error) {
|
||||
console.error('Failed to confirm transaction:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消记账
|
||||
const handleCancel = () => {
|
||||
setMessages(prev => {
|
||||
const updated = prev.map((msg, idx) => {
|
||||
if (idx === prev.length - 1 && msg.confirmationCard) {
|
||||
return { ...msg, confirmationCard: undefined };
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
|
||||
const cancelMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'assistant',
|
||||
content: '好的,这笔账不记了。还有什么需要帮忙的吗?',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages(prev => [...prev, cancelMessage]);
|
||||
|
||||
aiService.clearSession();
|
||||
setSessionId(null);
|
||||
};
|
||||
|
||||
// 语音录制
|
||||
const handleVoiceToggle = async () => {
|
||||
if (isRecording) {
|
||||
// 停止录音
|
||||
mediaRecorderRef.current?.stop();
|
||||
setIsRecording(false);
|
||||
} else {
|
||||
// 开始录音
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorderRef.current = mediaRecorder;
|
||||
audioChunksRef.current = [];
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
audioChunksRef.current.push(event.data);
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
|
||||
// 处理语音
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await aiService.processVoiceInput(audioBlob);
|
||||
|
||||
// 添加用户消息(使用转录文本)
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: '🎤 [语音消息]',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
|
||||
setSessionId(response.sessionId);
|
||||
|
||||
const assistantMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: response.message,
|
||||
confirmationCard: response.confirmationCard,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
} catch (error) {
|
||||
console.error('Voice processing failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
setIsRecording(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to start recording:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理快捷建议
|
||||
const handleSuggestion = (text: string) => {
|
||||
setInputText(text);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
// 处理回车发送
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-page">
|
||||
{/* Header */}
|
||||
<header className="chat-header">
|
||||
<button className="chat-header-back" onClick={() => navigate(-1)}>
|
||||
<FiArrowLeft size={20} />
|
||||
</button>
|
||||
<div className="chat-header-avatar">🪙</div>
|
||||
<div className="chat-header-info">
|
||||
<div className="chat-header-name">小金</div>
|
||||
<div className="chat-header-status">你的智能记账助手</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="chat-messages">
|
||||
{messages.length === 0 ? (
|
||||
<div className="chat-empty">
|
||||
<div className="chat-empty-icon">🪙</div>
|
||||
<div className="chat-empty-title">我是小金,你的记账助手</div>
|
||||
<div className="chat-empty-desc">
|
||||
告诉我你想买什么、花了多少,我会帮你分析并记账
|
||||
</div>
|
||||
<div className="chat-empty-suggestions">
|
||||
<button
|
||||
className="chat-suggestion-chip"
|
||||
onClick={() => handleSuggestion('想喝杯奶茶,15块')}
|
||||
>
|
||||
想喝杯奶茶,15块
|
||||
</button>
|
||||
<button
|
||||
className="chat-suggestion-chip"
|
||||
onClick={() => handleSuggestion('中午点了外卖 35元')}
|
||||
>
|
||||
中午点了外卖 35元
|
||||
</button>
|
||||
<button
|
||||
className="chat-suggestion-chip"
|
||||
onClick={() => handleSuggestion('想买个耳机 299')}
|
||||
>
|
||||
想买个耳机 299
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`chat-message ${msg.role}`}>
|
||||
<div className="chat-message-avatar">
|
||||
{msg.role === 'assistant' ? '🪙' : '👤'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="chat-message-content">{msg.content}</div>
|
||||
|
||||
{msg.confirmationCard && (
|
||||
<div className="confirmation-card">
|
||||
<div className="confirmation-card-header">
|
||||
<span className="confirmation-card-icon">📝</span>
|
||||
确认记账
|
||||
</div>
|
||||
<div className="confirmation-card-fields">
|
||||
<div className="confirmation-card-field">
|
||||
<div className="confirmation-card-label">金额</div>
|
||||
<div className={`confirmation-card-value amount ${msg.confirmationCard.type === 'income' ? 'income' : ''}`}>
|
||||
{msg.confirmationCard.type === 'income' ? '+' : '-'}
|
||||
¥{msg.confirmationCard.amount.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirmation-card-field">
|
||||
<div className="confirmation-card-label">类型</div>
|
||||
<div className="confirmation-card-value">
|
||||
{msg.confirmationCard.type === 'income' ? '收入' : '支出'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirmation-card-field">
|
||||
<div className="confirmation-card-label">分类</div>
|
||||
<div className="confirmation-card-value">{msg.confirmationCard.category}</div>
|
||||
</div>
|
||||
<div className="confirmation-card-field">
|
||||
<div className="confirmation-card-label">账户</div>
|
||||
<div className="confirmation-card-value">{msg.confirmationCard.account}</div>
|
||||
</div>
|
||||
{msg.confirmationCard.note && (
|
||||
<div className="confirmation-card-field full-width">
|
||||
<div className="confirmation-card-label">备注</div>
|
||||
<div className="confirmation-card-value">{msg.confirmationCard.note}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="confirmation-card-actions">
|
||||
<button
|
||||
className="confirmation-card-btn cancel"
|
||||
onClick={handleCancel}
|
||||
disabled={isLoading}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="confirmation-card-btn confirm"
|
||||
onClick={() => handleConfirm(msg.confirmationCard!)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
确认记账
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
<div className="chat-message assistant">
|
||||
<div className="chat-message-avatar">🪙</div>
|
||||
<div className="chat-loading">
|
||||
<div className="chat-loading-dots">
|
||||
<div className="chat-loading-dot"></div>
|
||||
<div className="chat-loading-dot"></div>
|
||||
<div className="chat-loading-dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="chat-input-area">
|
||||
<div className="chat-input-container">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
className="chat-input"
|
||||
placeholder="说说你想买什么..."
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={1}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
className={`chat-input-btn ${isRecording ? 'recording' : ''}`}
|
||||
onClick={handleVoiceToggle}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isRecording ? <FiMicOff size={20} /> : <FiMic size={20} />}
|
||||
</button>
|
||||
<button
|
||||
className="chat-input-btn send"
|
||||
onClick={handleSend}
|
||||
disabled={!inputText.trim() || isLoading}
|
||||
>
|
||||
<FiSend size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
src/pages/Chat/index.ts
Normal file
1
src/pages/Chat/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Chat';
|
||||
@@ -17,6 +17,7 @@ import Profile from '../pages/Profile';
|
||||
import GitHubCallback from '../pages/GitHubCallback';
|
||||
import GiteeCallback from '../pages/GiteeCallback';
|
||||
import Notifications from '../pages/Notifications';
|
||||
import Chat from '../pages/Chat';
|
||||
import Layout from '../components/common/Layout';
|
||||
import ProtectedRoute from '../components/common/ProtectedRoute';
|
||||
|
||||
@@ -53,6 +54,10 @@ export const router = createBrowserRouter([
|
||||
path: 'home',
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: 'chat',
|
||||
element: <Chat />,
|
||||
},
|
||||
{
|
||||
path: 'transactions',
|
||||
element: <Transactions />,
|
||||
|
||||
Reference in New Issue
Block a user