feat(ui): 重构模式选择器为点击切换交互
- 将 Build/Plan 下拉框和 Auto-approve 开关合并为三种模式 - Plan: 调用 plan agent,只读分析 - Ask: 调用 general agent,执行操作前需确认 - Auto: 调用 general agent,自动执行无需确认 - 点击按钮即可循环切换:Ask → Auto → Plan - 每种模式有独特的颜色和图标便于区分
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Send, Square } from 'lucide-react';
|
||||
import { Square, Sparkles } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import clsx from 'clsx';
|
||||
import { CommandMenu, type CommandMenuItem } from './CommandMenu.js';
|
||||
import { FileMenu, type FileMenuItem } from './FileMenu.js';
|
||||
@@ -247,7 +248,7 @@ export function ChatInput({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'border-t border-line bg-surface-base relative',
|
||||
'bg-surface-base relative',
|
||||
responsive ? 'p-3 md:p-4 safe-area-pb' : 'p-4'
|
||||
)}
|
||||
>
|
||||
@@ -278,83 +279,116 @@ export function ChatInput({
|
||||
)}
|
||||
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* 已选文件标签 */}
|
||||
{mentionedFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||
{mentionedFiles.map((file, index) => (
|
||||
<FileMentionTag
|
||||
key={`${file}-${index}`}
|
||||
path={file}
|
||||
size="sm"
|
||||
removable
|
||||
onRemove={() => handleRemoveFile(file)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="flex gap-2 items-end">
|
||||
{/* Agent 模式选择器 */}
|
||||
{onAgentModeChange && (
|
||||
<div className="flex-shrink-0 pb-1.5">
|
||||
<AgentModeSelector
|
||||
mode={agentMode}
|
||||
onModeChange={onAgentModeChange}
|
||||
autoApprove={autoApprove}
|
||||
onAutoApproveChange={onAutoApproveChange ?? (() => {})}
|
||||
disabled={disabled || isLoading}
|
||||
/>
|
||||
{/* 主输入容器 - 现代化卡片设计 */}
|
||||
<div
|
||||
className={clsx(
|
||||
'relative rounded-2xl border transition-all duration-200',
|
||||
'bg-surface-subtle',
|
||||
disabled
|
||||
? 'border-line opacity-60'
|
||||
: 'border-line hover:border-fg-subtle/30 focus-within:border-primary-500 focus-within:shadow-lg focus-within:shadow-primary-500/10'
|
||||
)}
|
||||
>
|
||||
{/* 已选文件标签 */}
|
||||
{mentionedFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 px-4 pt-3">
|
||||
{mentionedFiles.map((file, index) => (
|
||||
<FileMentionTag
|
||||
key={`${file}-${index}`}
|
||||
path={file}
|
||||
size="sm"
|
||||
removable
|
||||
onRemove={() => handleRemoveFile(file)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
responsive
|
||||
? 'Type a message, /command, or @file...'
|
||||
: 'Type a message, /command, or @file... (Shift+Enter for new line)'
|
||||
}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
className={clsx(
|
||||
'w-full resize-none rounded-lg border border-line bg-surface-subtle',
|
||||
responsive ? 'px-3 py-2.5 md:px-4 md:py-3' : 'px-4 py-3',
|
||||
responsive ? 'text-base md:text-sm' : 'text-sm', // 移动端使用 16px 防止缩放
|
||||
'text-fg placeholder-fg-subtle',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={isLoading ? onCancel : handleSubmit}
|
||||
disabled={!isLoading && (!input.trim() || disabled)}
|
||||
className={clsx(
|
||||
'rounded-lg flex items-center justify-center transition-colors self-end',
|
||||
responsive
|
||||
? 'px-3 py-2.5 md:px-4 md:py-3 min-w-[44px] min-h-[44px]' // 最小触摸目标 44x44
|
||||
: 'px-4 py-3',
|
||||
isLoading
|
||||
? 'bg-red-600 hover:bg-red-700 active:bg-red-800 text-white'
|
||||
: 'bg-primary-600 hover:bg-primary-700 active:bg-primary-800 text-white',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
{/* 输入区域 */}
|
||||
<div className="flex items-end gap-2 p-2">
|
||||
{/* Agent 模式选择器 */}
|
||||
{onAgentModeChange && (
|
||||
<div className="flex-shrink-0 mb-1">
|
||||
<AgentModeSelector
|
||||
mode={agentMode}
|
||||
onModeChange={onAgentModeChange}
|
||||
autoApprove={autoApprove}
|
||||
onAutoApproveChange={onAutoApproveChange ?? (() => {})}
|
||||
disabled={disabled || isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{isLoading ? <Square size={20} /> : <Send size={20} />}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
responsive
|
||||
? 'Ask anything...'
|
||||
: 'Ask anything... (Shift+Enter for new line)'
|
||||
}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
className={clsx(
|
||||
'w-full resize-none bg-transparent border-0',
|
||||
responsive ? 'px-2 py-2 md:px-3 md:py-2' : 'px-3 py-2',
|
||||
responsive ? 'text-base md:text-sm' : 'text-sm',
|
||||
'text-fg placeholder-fg-subtle/60',
|
||||
'focus:outline-none focus:ring-0',
|
||||
'disabled:cursor-not-allowed'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 发送按钮 */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={isLoading ? onCancel : handleSubmit}
|
||||
disabled={!isLoading && (!input.trim() || disabled)}
|
||||
className={clsx(
|
||||
'flex-shrink-0 rounded-xl flex items-center justify-center transition-all duration-200',
|
||||
responsive
|
||||
? 'w-10 h-10 md:w-9 md:h-9'
|
||||
: 'w-9 h-9',
|
||||
isLoading
|
||||
? 'bg-red-500 hover:bg-red-600 text-white shadow-lg shadow-red-500/25'
|
||||
: input.trim() && !disabled
|
||||
? 'bg-gradient-to-r from-primary-500 to-primary-600 hover:from-primary-600 hover:to-primary-700 text-white shadow-lg shadow-primary-500/25'
|
||||
: 'bg-surface-muted text-fg-subtle',
|
||||
'disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none'
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Square size={18} className="fill-current" />
|
||||
) : (
|
||||
<Sparkles size={18} className={clsx(input.trim() && !disabled && 'animate-pulse')} />
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部提示 */}
|
||||
{responsive && (
|
||||
<div className="hidden md:flex items-center justify-center gap-4 mt-3 text-xs text-fg-subtle/60">
|
||||
<span className="flex items-center gap-1">
|
||||
<kbd className="px-1.5 py-0.5 rounded bg-surface-subtle text-fg-muted font-mono text-[10px]">Enter</kbd>
|
||||
<span>send</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<kbd className="px-1.5 py-0.5 rounded bg-surface-subtle text-fg-muted font-mono text-[10px]">/</kbd>
|
||||
<span>commands</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<kbd className="px-1.5 py-0.5 rounded bg-surface-subtle text-fg-muted font-mono text-[10px]">@</kbd>
|
||||
<span>files</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 响应式模式下桌面端显示提示文字 */}
|
||||
{responsive && (
|
||||
<p className="hidden md:block text-xs text-fg-subtle text-center mt-2">
|
||||
Press Enter to send, Shift+Enter for new line, / for commands, @ for files
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user