feat(ui): 添加斜杠命令输入支持
- 新增 useCommands hook 用于加载和搜索命令 - 新增 CommandMenu 组件,支持键盘导航和选择 - ChatInput 支持 / 触发命令菜单 - 导出命令相关 API 和类型
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Commands Hook
|
||||
*
|
||||
* 管理斜杠命令的加载、搜索和缓存
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { listCommands, searchCommands } from '../api/client.js';
|
||||
import type { CommandListResponse } from '../api/types.js';
|
||||
|
||||
type CommandItem = CommandListResponse['commands'][number];
|
||||
|
||||
interface UseCommandsOptions {
|
||||
/** 是否在挂载时自动加载命令列表 */
|
||||
autoLoad?: boolean;
|
||||
}
|
||||
|
||||
interface UseCommandsState {
|
||||
/** 所有命令列表 */
|
||||
commands: CommandItem[];
|
||||
/** 过滤后的命令列表 */
|
||||
filteredCommands: CommandItem[];
|
||||
/** 是否正在加载 */
|
||||
isLoading: boolean;
|
||||
/** 错误信息 */
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function useCommands(options: UseCommandsOptions = {}) {
|
||||
const { autoLoad = true } = options;
|
||||
|
||||
const [state, setState] = useState<UseCommandsState>({
|
||||
commands: [],
|
||||
filteredCommands: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const loadedRef = useRef(false);
|
||||
|
||||
// 加载命令列表
|
||||
const loadCommands = useCallback(async () => {
|
||||
if (state.isLoading) return;
|
||||
|
||||
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
const result = await listCommands();
|
||||
if (result.success) {
|
||||
setState({
|
||||
commands: result.data.commands,
|
||||
filteredCommands: result.data.commands,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: 'Failed to load commands',
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
}));
|
||||
}
|
||||
}, [state.isLoading]);
|
||||
|
||||
// 搜索/过滤命令
|
||||
const filterCommands = useCallback(
|
||||
async (query: string) => {
|
||||
// 空查询显示所有命令
|
||||
if (!query.trim()) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
filteredCommands: prev.commands,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地过滤(快速响应)
|
||||
const queryLower = query.toLowerCase();
|
||||
const localFiltered = state.commands.filter(
|
||||
(cmd) =>
|
||||
cmd.name.toLowerCase().includes(queryLower) ||
|
||||
cmd.description?.toLowerCase().includes(queryLower)
|
||||
);
|
||||
|
||||
// 如果本地过滤有结果,直接使用
|
||||
if (localFiltered.length > 0) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
filteredCommands: localFiltered,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则尝试服务端搜索(可能有模糊匹配)
|
||||
try {
|
||||
const result = await searchCommands(query, 10);
|
||||
if (result.success && result.data.length > 0) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
filteredCommands: result.data.map((r) => ({
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
source: r.source,
|
||||
})),
|
||||
}));
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
filteredCommands: [],
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// 搜索失败,保持本地过滤结果
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
filteredCommands: localFiltered,
|
||||
}));
|
||||
}
|
||||
},
|
||||
[state.commands]
|
||||
);
|
||||
|
||||
// 自动加载
|
||||
useEffect(() => {
|
||||
if (autoLoad && !loadedRef.current) {
|
||||
loadedRef.current = true;
|
||||
loadCommands();
|
||||
}
|
||||
}, [autoLoad, loadCommands]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
loadCommands,
|
||||
filterCommands,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user