From db711648e0885ba82ebb3a0160eed02f1e761a7c Mon Sep 17 00:00:00 2001 From: kurihada Date: Fri, 12 Dec 2025 18:38:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=E6=96=9C?= =?UTF-8?q?=E6=9D=A0=E5=91=BD=E4=BB=A4=E8=BE=93=E5=85=A5=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 useCommands hook 用于加载和搜索命令 - 新增 CommandMenu 组件,支持键盘导航和选择 - ChatInput 支持 / 触发命令菜单 - 导出命令相关 API 和类型 --- packages/ui/src/components/ChatInput.tsx | 105 ++++++++- packages/ui/src/components/CommandMenu.tsx | 245 +++++++++++++++++++++ packages/ui/src/hooks/useCommands.ts | 143 ++++++++++++ packages/ui/src/index.ts | 13 ++ 4 files changed, 500 insertions(+), 6 deletions(-) create mode 100644 packages/ui/src/components/CommandMenu.tsx create mode 100644 packages/ui/src/hooks/useCommands.ts diff --git a/packages/ui/src/components/ChatInput.tsx b/packages/ui/src/components/ChatInput.tsx index 7cd128e..ade207c 100644 --- a/packages/ui/src/components/ChatInput.tsx +++ b/packages/ui/src/components/ChatInput.tsx @@ -2,11 +2,14 @@ * Chat Input Component * * 支持响应式:responsive=true 时适配移动端键盘和触摸操作 + * 支持斜杠命令:输入 / 时显示命令菜单 */ -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; import { Send, Square } from 'lucide-react'; import clsx from 'clsx'; +import { CommandMenu, type CommandMenuItem } from './CommandMenu.js'; +import { useCommands } from '../hooks/useCommands.js'; interface ChatInputProps { onSend: (content: string) => void; @@ -15,6 +18,8 @@ interface ChatInputProps { disabled?: boolean; /** 是否启用响应式布局(移动端适配) */ responsive?: boolean; + /** 是否启用斜杠命令 */ + enableCommands?: boolean; } export function ChatInput({ @@ -23,10 +28,20 @@ export function ChatInput({ isLoading, disabled, responsive = false, + enableCommands = true, }: ChatInputProps) { const [input, setInput] = useState(''); + const [showCommandMenu, setShowCommandMenu] = useState(false); + const [selectedCommandIndex, setSelectedCommandIndex] = useState(0); const textareaRef = useRef(null); + // 命令系统 + const { + filteredCommands, + isLoading: commandsLoading, + filterCommands, + } = useCommands({ autoLoad: enableCommands }); + // 自动调整高度 useEffect(() => { const textarea = textareaRef.current; @@ -38,10 +53,59 @@ export function ChatInput({ } }, [input, responsive]); + // 检测斜杠命令输入 + const checkCommandTrigger = useCallback( + (value: string) => { + if (!enableCommands) return; + + // 检测是否在输入斜杠命令 + // 条件:以 / 开头,且 / 后面没有空格(还在输入命令名) + const slashMatch = value.match(/^\/(\S*)$/); + + if (slashMatch) { + const query = slashMatch[1]; // / 后面的内容 + setShowCommandMenu(true); + setSelectedCommandIndex(0); + filterCommands(query); + } else { + setShowCommandMenu(false); + } + }, + [enableCommands, filterCommands] + ); + + // 处理输入变化 + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setInput(value); + checkCommandTrigger(value); + }; + + // 选择命令 + const handleCommandSelect = useCallback( + (command: CommandMenuItem) => { + // 替换输入内容为 /command + 空格,准备输入参数 + setInput(`/${command.name} `); + setShowCommandMenu(false); + + // 聚焦输入框 + textareaRef.current?.focus(); + }, + [] + ); + + // 关闭命令菜单 + const handleCommandMenuClose = useCallback(() => { + setShowCommandMenu(false); + }, []); + const handleSubmit = () => { const trimmed = input.trim(); if (!trimmed || isLoading || disabled) return; + // 关闭命令菜单 + setShowCommandMenu(false); + onSend(trimmed); setInput(''); @@ -52,6 +116,22 @@ export function ChatInput({ }; const handleKeyDown = (e: React.KeyboardEvent) => { + // 如果命令菜单打开,让菜单处理键盘事件 + if (showCommandMenu && filteredCommands.length > 0) { + if (['ArrowUp', 'ArrowDown', 'Tab', 'Escape'].includes(e.key)) { + // 这些键由 CommandMenu 处理,阻止默认行为 + return; + } + if (e.key === 'Enter' && !e.shiftKey) { + // Enter 选择命令 + e.preventDefault(); + if (filteredCommands[selectedCommandIndex]) { + handleCommandSelect(filteredCommands[selectedCommandIndex]); + } + return; + } + } + // Enter 发送,Shift+Enter 换行 if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -62,21 +142,34 @@ export function ChatInput({ return (
+ {/* Command Menu */} + {enableCommands && ( + + )} +