feat: 前端输入框体验优化
- AI 响应完成后自动聚焦输入框 - 输入框和发送按钮居中对齐 - 隐藏输入框滚动条 - 新建话题无需输入名称,自动生成默认标题
This commit is contained in:
parent
06756a4816
commit
3d9c981c2a
@ -142,12 +142,9 @@ function App() {
|
||||
return
|
||||
}
|
||||
|
||||
const title = prompt('Enter topic title:')
|
||||
if (title) {
|
||||
const cmd = createTopic(title)
|
||||
const cmd = createTopic()
|
||||
handleCommand(cmd)
|
||||
sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
|
||||
}
|
||||
}, [sendMessage, handleCommand, createTopic, sessionId, isReadOnly])
|
||||
|
||||
const handleSwitchTopic = useCallback(
|
||||
|
||||
@ -27,6 +27,7 @@ export function ChatContainer({
|
||||
<MessageInput
|
||||
onSend={onSendMessage}
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
isReadOnly={isReadOnly}
|
||||
channelName={channelName}
|
||||
/>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useState, useRef, useEffect } from 'react'
|
||||
interface MessageInputProps {
|
||||
onSend: (content: string) => void
|
||||
disabled?: boolean
|
||||
isLoading?: boolean
|
||||
placeholder?: string
|
||||
isReadOnly?: boolean
|
||||
channelName?: string
|
||||
@ -12,12 +13,14 @@ interface MessageInputProps {
|
||||
export function MessageInput({
|
||||
onSend,
|
||||
disabled = false,
|
||||
isLoading = false,
|
||||
placeholder = '输入消息...按 / 查看命令',
|
||||
isReadOnly = false,
|
||||
channelName,
|
||||
}: MessageInputProps) {
|
||||
const [content, setContent] = useState('')
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const wasLoadingRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current
|
||||
@ -27,6 +30,14 @@ export function MessageInput({
|
||||
}
|
||||
}, [content])
|
||||
|
||||
// 当 isLoading 从 true 变为 false 时,自动聚焦输入框
|
||||
useEffect(() => {
|
||||
if (wasLoadingRef.current && !isLoading && !isReadOnly) {
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
wasLoadingRef.current = isLoading
|
||||
}, [isLoading, isReadOnly])
|
||||
|
||||
const handleSend = () => {
|
||||
if (content.trim() && !disabled && !isReadOnly) {
|
||||
onSend(content.trim())
|
||||
@ -74,8 +85,8 @@ export function MessageInput({
|
||||
|
||||
return (
|
||||
<div className="border-t border-white/8 bg-[#12121a]/80 backdrop-blur-md p-4">
|
||||
<div className="flex gap-3 items-end max-w-5xl mx-auto">
|
||||
<div className="flex-1 relative">
|
||||
<div className="flex gap-3 items-center max-w-5xl mx-auto">
|
||||
<div className="flex-1 relative flex items-center">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={content}
|
||||
@ -84,14 +95,14 @@ export function MessageInput({
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
className="w-full resize-none rounded-xl border border-white/10 bg-[#1a1a25] px-4 py-3 pr-12 text-sm text-white placeholder:text-zinc-500 focus:border-[#00f0ff]/50 focus:outline-none focus:ring-1 focus:ring-[#00f0ff]/20 disabled:opacity-50 transition-all"
|
||||
className="w-full resize-none rounded-xl border border-white/10 bg-[#1a1a25] px-4 py-3 pr-12 text-sm text-white placeholder:text-zinc-500 focus:border-[#00f0ff]/50 focus:outline-none focus:ring-1 focus:ring-[#00f0ff]/20 disabled:opacity-50 transition-all self-center scrollbar-hide"
|
||||
/>
|
||||
<Sparkles className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-600 pointer-events-none" />
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={disabled || !content.trim()}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-r from-[#00f0ff] to-[#3b82f6] text-white shadow-lg shadow-[#00f0ff]/20 hover:shadow-xl hover:shadow-[#00f0ff]/30 hover:scale-105 disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed transition-all"
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-r from-[#00f0ff] to-[#3b82f6] text-white shadow-lg shadow-[#00f0ff]/20 hover:shadow-xl hover:shadow-[#00f0ff]/30 hover:scale-105 disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
{disabled ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@ -47,7 +47,7 @@ interface UseChatReturn {
|
||||
|
||||
// Topic 方法
|
||||
selectTopic: (topicId: string) => void
|
||||
createTopic: (title: string) => Command
|
||||
createTopic: (title?: string) => Command
|
||||
switchTopic: (topicId: string) => Command
|
||||
|
||||
// 初始化方法
|
||||
@ -425,10 +425,10 @@ export function useChat(): UseChatReturn {
|
||||
setMessages([])
|
||||
}, [])
|
||||
|
||||
const createTopic = useCallback((title: string): Command => {
|
||||
const createTopic = useCallback((title?: string): Command => {
|
||||
return {
|
||||
type: 'create_session',
|
||||
title,
|
||||
title: title || `话题 ${new Date().toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`,
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@ -45,6 +45,16 @@
|
||||
background: var(--accent-blue);
|
||||
}
|
||||
|
||||
/* Hide scrollbar utility */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user