feat(ui): add thinking mode selector to floating prompt input
Add comprehensive thinking mode selection with 5 levels (Auto, Think, Think Hard, Think Harder, Ultrathink) to the FloatingPromptInput component. Features include: - New ThinkingModeIndicator component with visual level bars - Thinking mode picker with tooltips and descriptions - Automatic phrase appending to prompts based on selected mode - Brain icon integration and enhanced UI layout - State management for thinking mode selection This enhancement allows users to control Claude's reasoning depth directly from the prompt interface.
This commit is contained in:
@@ -7,12 +7,14 @@ import {
|
||||
ChevronUp,
|
||||
Sparkles,
|
||||
Zap,
|
||||
Square
|
||||
Square,
|
||||
Brain
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover } from "@/components/ui/popover";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { FilePicker } from "./FilePicker";
|
||||
import { ImagePreview } from "./ImagePreview";
|
||||
import { type FileEntry } from "@/lib/api";
|
||||
@@ -53,6 +55,78 @@ export interface FloatingPromptInputRef {
|
||||
addImage: (imagePath: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thinking mode type definition
|
||||
*/
|
||||
type ThinkingMode = "auto" | "think" | "think_hard" | "think_harder" | "ultrathink";
|
||||
|
||||
/**
|
||||
* Thinking mode configuration
|
||||
*/
|
||||
type ThinkingModeConfig = {
|
||||
id: ThinkingMode;
|
||||
name: string;
|
||||
description: string;
|
||||
level: number; // 0-4 for visual indicator
|
||||
phrase?: string; // The phrase to append
|
||||
};
|
||||
|
||||
const THINKING_MODES: ThinkingModeConfig[] = [
|
||||
{
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
description: "Let Claude decide",
|
||||
level: 0
|
||||
},
|
||||
{
|
||||
id: "think",
|
||||
name: "Think",
|
||||
description: "Basic reasoning",
|
||||
level: 1,
|
||||
phrase: "think"
|
||||
},
|
||||
{
|
||||
id: "think_hard",
|
||||
name: "Think Hard",
|
||||
description: "Deeper analysis",
|
||||
level: 2,
|
||||
phrase: "think hard"
|
||||
},
|
||||
{
|
||||
id: "think_harder",
|
||||
name: "Think Harder",
|
||||
description: "Extensive reasoning",
|
||||
level: 3,
|
||||
phrase: "think harder"
|
||||
},
|
||||
{
|
||||
id: "ultrathink",
|
||||
name: "Ultrathink",
|
||||
description: "Maximum computation",
|
||||
level: 4,
|
||||
phrase: "ultrathink"
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* ThinkingModeIndicator component - Shows visual indicator bars for thinking level
|
||||
*/
|
||||
const ThinkingModeIndicator: React.FC<{ level: number }> = ({ level }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
"w-1 h-3 rounded-full transition-colors",
|
||||
i <= level ? "bg-blue-500" : "bg-muted"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Model = {
|
||||
id: "sonnet" | "opus";
|
||||
name: string;
|
||||
@@ -100,8 +174,10 @@ const FloatingPromptInputInner = (
|
||||
) => {
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel);
|
||||
const [selectedThinkingMode, setSelectedThinkingMode] = useState<ThinkingMode>("auto");
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
||||
const [thinkingModePickerOpen, setThinkingModePickerOpen] = useState(false);
|
||||
const [showFilePicker, setShowFilePicker] = useState(false);
|
||||
const [filePickerQuery, setFilePickerQuery] = useState("");
|
||||
const [cursorPosition, setCursorPosition] = useState(0);
|
||||
@@ -260,7 +336,15 @@ const FloatingPromptInputInner = (
|
||||
|
||||
const handleSend = () => {
|
||||
if (prompt.trim() && !isLoading && !disabled) {
|
||||
onSend(prompt.trim(), selectedModel);
|
||||
let finalPrompt = prompt.trim();
|
||||
|
||||
// Append thinking phrase if not auto mode
|
||||
const thinkingMode = THINKING_MODES.find(m => m.id === selectedThinkingMode);
|
||||
if (thinkingMode && thinkingMode.phrase) {
|
||||
finalPrompt = `${finalPrompt}\n\n${thinkingMode.phrase}.`;
|
||||
}
|
||||
|
||||
onSend(finalPrompt, selectedModel);
|
||||
setPrompt("");
|
||||
setEmbeddedImages([]);
|
||||
}
|
||||
@@ -440,17 +524,81 @@ const FloatingPromptInputInner = (
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Model:</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setModelPickerOpen(!modelPickerOpen)}
|
||||
className="gap-2"
|
||||
>
|
||||
{selectedModelData.icon}
|
||||
{selectedModelData.name}
|
||||
</Button>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Model:</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setModelPickerOpen(!modelPickerOpen)}
|
||||
className="gap-2"
|
||||
>
|
||||
{selectedModelData.icon}
|
||||
{selectedModelData.name}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Thinking:</span>
|
||||
<Popover
|
||||
trigger={
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setThinkingModePickerOpen(!thinkingModePickerOpen)}
|
||||
className="gap-2"
|
||||
>
|
||||
<Brain className="h-4 w-4" />
|
||||
<ThinkingModeIndicator
|
||||
level={THINKING_MODES.find(m => m.id === selectedThinkingMode)?.level || 0}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="font-medium">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || "Auto"}</p>
|
||||
<p className="text-xs text-muted-foreground">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
}
|
||||
content={
|
||||
<div className="w-[280px] p-1">
|
||||
{THINKING_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
onClick={() => {
|
||||
setSelectedThinkingMode(mode.id);
|
||||
setThinkingModePickerOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"w-full flex items-start gap-3 p-3 rounded-md transition-colors text-left",
|
||||
"hover:bg-accent",
|
||||
selectedThinkingMode === mode.id && "bg-accent"
|
||||
)}
|
||||
>
|
||||
<Brain className="h-4 w-4 mt-0.5" />
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="font-medium text-sm">
|
||||
{mode.name}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{mode.description}
|
||||
</div>
|
||||
</div>
|
||||
<ThinkingModeIndicator level={mode.level} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
open={thinkingModePickerOpen}
|
||||
onOpenChange={setThinkingModePickerOpen}
|
||||
align="start"
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -541,6 +689,66 @@ const FloatingPromptInputInner = (
|
||||
side="top"
|
||||
/>
|
||||
|
||||
{/* Thinking Mode Picker */}
|
||||
<Popover
|
||||
trigger={
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
disabled={isLoading || disabled}
|
||||
className="gap-2"
|
||||
>
|
||||
<Brain className="h-4 w-4" />
|
||||
<ThinkingModeIndicator
|
||||
level={THINKING_MODES.find(m => m.id === selectedThinkingMode)?.level || 0}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="font-medium">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || "Auto"}</p>
|
||||
<p className="text-xs text-muted-foreground">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
}
|
||||
content={
|
||||
<div className="w-[280px] p-1">
|
||||
{THINKING_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
onClick={() => {
|
||||
setSelectedThinkingMode(mode.id);
|
||||
setThinkingModePickerOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"w-full flex items-start gap-3 p-3 rounded-md transition-colors text-left",
|
||||
"hover:bg-accent",
|
||||
selectedThinkingMode === mode.id && "bg-accent"
|
||||
)}
|
||||
>
|
||||
<Brain className="h-4 w-4 mt-0.5" />
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="font-medium text-sm">
|
||||
{mode.name}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{mode.description}
|
||||
</div>
|
||||
</div>
|
||||
<ThinkingModeIndicator level={mode.level} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
open={thinkingModePickerOpen}
|
||||
onOpenChange={setThinkingModePickerOpen}
|
||||
align="start"
|
||||
side="top"
|
||||
/>
|
||||
|
||||
{/* Prompt Input */}
|
||||
<div className="flex-1 relative">
|
||||
<Textarea
|
||||
|
Reference in New Issue
Block a user