Skip to content

Commit b446122

Browse files
committed
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.
1 parent fa691df commit b446122

File tree

1 file changed

+221
-13
lines changed

1 file changed

+221
-13
lines changed

src/components/FloatingPromptInput.tsx

Lines changed: 221 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import {
77
ChevronUp,
88
Sparkles,
99
Zap,
10-
Square
10+
Square,
11+
Brain
1112
} from "lucide-react";
1213
import { cn } from "@/lib/utils";
1314
import { Button } from "@/components/ui/button";
1415
import { Popover } from "@/components/ui/popover";
1516
import { Textarea } from "@/components/ui/textarea";
17+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
1618
import { FilePicker } from "./FilePicker";
1719
import { ImagePreview } from "./ImagePreview";
1820
import { type FileEntry } from "@/lib/api";
@@ -53,6 +55,78 @@ export interface FloatingPromptInputRef {
5355
addImage: (imagePath: string) => void;
5456
}
5557

58+
/**
59+
* Thinking mode type definition
60+
*/
61+
type ThinkingMode = "auto" | "think" | "think_hard" | "think_harder" | "ultrathink";
62+
63+
/**
64+
* Thinking mode configuration
65+
*/
66+
type ThinkingModeConfig = {
67+
id: ThinkingMode;
68+
name: string;
69+
description: string;
70+
level: number; // 0-4 for visual indicator
71+
phrase?: string; // The phrase to append
72+
};
73+
74+
const THINKING_MODES: ThinkingModeConfig[] = [
75+
{
76+
id: "auto",
77+
name: "Auto",
78+
description: "Let Claude decide",
79+
level: 0
80+
},
81+
{
82+
id: "think",
83+
name: "Think",
84+
description: "Basic reasoning",
85+
level: 1,
86+
phrase: "think"
87+
},
88+
{
89+
id: "think_hard",
90+
name: "Think Hard",
91+
description: "Deeper analysis",
92+
level: 2,
93+
phrase: "think hard"
94+
},
95+
{
96+
id: "think_harder",
97+
name: "Think Harder",
98+
description: "Extensive reasoning",
99+
level: 3,
100+
phrase: "think harder"
101+
},
102+
{
103+
id: "ultrathink",
104+
name: "Ultrathink",
105+
description: "Maximum computation",
106+
level: 4,
107+
phrase: "ultrathink"
108+
}
109+
];
110+
111+
/**
112+
* ThinkingModeIndicator component - Shows visual indicator bars for thinking level
113+
*/
114+
const ThinkingModeIndicator: React.FC<{ level: number }> = ({ level }) => {
115+
return (
116+
<div className="flex items-center gap-0.5">
117+
{[1, 2, 3, 4].map((i) => (
118+
<div
119+
key={i}
120+
className={cn(
121+
"w-1 h-3 rounded-full transition-colors",
122+
i <= level ? "bg-blue-500" : "bg-muted"
123+
)}
124+
/>
125+
))}
126+
</div>
127+
);
128+
};
129+
56130
type Model = {
57131
id: "sonnet" | "opus";
58132
name: string;
@@ -100,8 +174,10 @@ const FloatingPromptInputInner = (
100174
) => {
101175
const [prompt, setPrompt] = useState("");
102176
const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel);
177+
const [selectedThinkingMode, setSelectedThinkingMode] = useState<ThinkingMode>("auto");
103178
const [isExpanded, setIsExpanded] = useState(false);
104179
const [modelPickerOpen, setModelPickerOpen] = useState(false);
180+
const [thinkingModePickerOpen, setThinkingModePickerOpen] = useState(false);
105181
const [showFilePicker, setShowFilePicker] = useState(false);
106182
const [filePickerQuery, setFilePickerQuery] = useState("");
107183
const [cursorPosition, setCursorPosition] = useState(0);
@@ -260,7 +336,15 @@ const FloatingPromptInputInner = (
260336

261337
const handleSend = () => {
262338
if (prompt.trim() && !isLoading && !disabled) {
263-
onSend(prompt.trim(), selectedModel);
339+
let finalPrompt = prompt.trim();
340+
341+
// Append thinking phrase if not auto mode
342+
const thinkingMode = THINKING_MODES.find(m => m.id === selectedThinkingMode);
343+
if (thinkingMode && thinkingMode.phrase) {
344+
finalPrompt = `${finalPrompt}\n\n${thinkingMode.phrase}.`;
345+
}
346+
347+
onSend(finalPrompt, selectedModel);
264348
setPrompt("");
265349
setEmbeddedImages([]);
266350
}
@@ -440,17 +524,81 @@ const FloatingPromptInputInner = (
440524
/>
441525

442526
<div className="flex items-center justify-between">
443-
<div className="flex items-center gap-2">
444-
<span className="text-xs text-muted-foreground">Model:</span>
445-
<Button
446-
variant="outline"
447-
size="sm"
448-
onClick={() => setModelPickerOpen(!modelPickerOpen)}
449-
className="gap-2"
450-
>
451-
{selectedModelData.icon}
452-
{selectedModelData.name}
453-
</Button>
527+
<div className="flex items-center gap-4">
528+
<div className="flex items-center gap-2">
529+
<span className="text-xs text-muted-foreground">Model:</span>
530+
<Button
531+
variant="outline"
532+
size="sm"
533+
onClick={() => setModelPickerOpen(!modelPickerOpen)}
534+
className="gap-2"
535+
>
536+
{selectedModelData.icon}
537+
{selectedModelData.name}
538+
</Button>
539+
</div>
540+
541+
<div className="flex items-center gap-2">
542+
<span className="text-xs text-muted-foreground">Thinking:</span>
543+
<Popover
544+
trigger={
545+
<TooltipProvider>
546+
<Tooltip>
547+
<TooltipTrigger asChild>
548+
<Button
549+
variant="outline"
550+
size="sm"
551+
onClick={() => setThinkingModePickerOpen(!thinkingModePickerOpen)}
552+
className="gap-2"
553+
>
554+
<Brain className="h-4 w-4" />
555+
<ThinkingModeIndicator
556+
level={THINKING_MODES.find(m => m.id === selectedThinkingMode)?.level || 0}
557+
/>
558+
</Button>
559+
</TooltipTrigger>
560+
<TooltipContent>
561+
<p className="font-medium">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || "Auto"}</p>
562+
<p className="text-xs text-muted-foreground">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}</p>
563+
</TooltipContent>
564+
</Tooltip>
565+
</TooltipProvider>
566+
}
567+
content={
568+
<div className="w-[280px] p-1">
569+
{THINKING_MODES.map((mode) => (
570+
<button
571+
key={mode.id}
572+
onClick={() => {
573+
setSelectedThinkingMode(mode.id);
574+
setThinkingModePickerOpen(false);
575+
}}
576+
className={cn(
577+
"w-full flex items-start gap-3 p-3 rounded-md transition-colors text-left",
578+
"hover:bg-accent",
579+
selectedThinkingMode === mode.id && "bg-accent"
580+
)}
581+
>
582+
<Brain className="h-4 w-4 mt-0.5" />
583+
<div className="flex-1 space-y-1">
584+
<div className="font-medium text-sm">
585+
{mode.name}
586+
</div>
587+
<div className="text-xs text-muted-foreground">
588+
{mode.description}
589+
</div>
590+
</div>
591+
<ThinkingModeIndicator level={mode.level} />
592+
</button>
593+
))}
594+
</div>
595+
}
596+
open={thinkingModePickerOpen}
597+
onOpenChange={setThinkingModePickerOpen}
598+
align="start"
599+
side="top"
600+
/>
601+
</div>
454602
</div>
455603

456604
<Button
@@ -541,6 +689,66 @@ const FloatingPromptInputInner = (
541689
side="top"
542690
/>
543691

692+
{/* Thinking Mode Picker */}
693+
<Popover
694+
trigger={
695+
<TooltipProvider>
696+
<Tooltip>
697+
<TooltipTrigger asChild>
698+
<Button
699+
variant="outline"
700+
size="default"
701+
disabled={isLoading || disabled}
702+
className="gap-2"
703+
>
704+
<Brain className="h-4 w-4" />
705+
<ThinkingModeIndicator
706+
level={THINKING_MODES.find(m => m.id === selectedThinkingMode)?.level || 0}
707+
/>
708+
</Button>
709+
</TooltipTrigger>
710+
<TooltipContent>
711+
<p className="font-medium">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || "Auto"}</p>
712+
<p className="text-xs text-muted-foreground">{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}</p>
713+
</TooltipContent>
714+
</Tooltip>
715+
</TooltipProvider>
716+
}
717+
content={
718+
<div className="w-[280px] p-1">
719+
{THINKING_MODES.map((mode) => (
720+
<button
721+
key={mode.id}
722+
onClick={() => {
723+
setSelectedThinkingMode(mode.id);
724+
setThinkingModePickerOpen(false);
725+
}}
726+
className={cn(
727+
"w-full flex items-start gap-3 p-3 rounded-md transition-colors text-left",
728+
"hover:bg-accent",
729+
selectedThinkingMode === mode.id && "bg-accent"
730+
)}
731+
>
732+
<Brain className="h-4 w-4 mt-0.5" />
733+
<div className="flex-1 space-y-1">
734+
<div className="font-medium text-sm">
735+
{mode.name}
736+
</div>
737+
<div className="text-xs text-muted-foreground">
738+
{mode.description}
739+
</div>
740+
</div>
741+
<ThinkingModeIndicator level={mode.level} />
742+
</button>
743+
))}
744+
</div>
745+
}
746+
open={thinkingModePickerOpen}
747+
onOpenChange={setThinkingModePickerOpen}
748+
align="start"
749+
side="top"
750+
/>
751+
544752
{/* Prompt Input */}
545753
<div className="flex-1 relative">
546754
<Textarea

0 commit comments

Comments
 (0)