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,
|
ChevronUp,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Zap,
|
Zap,
|
||||||
Square
|
Square,
|
||||||
|
Brain
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Popover } from "@/components/ui/popover";
|
import { Popover } from "@/components/ui/popover";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { FilePicker } from "./FilePicker";
|
import { FilePicker } from "./FilePicker";
|
||||||
import { ImagePreview } from "./ImagePreview";
|
import { ImagePreview } from "./ImagePreview";
|
||||||
import { type FileEntry } from "@/lib/api";
|
import { type FileEntry } from "@/lib/api";
|
||||||
@@ -53,6 +55,78 @@ export interface FloatingPromptInputRef {
|
|||||||
addImage: (imagePath: string) => void;
|
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 = {
|
type Model = {
|
||||||
id: "sonnet" | "opus";
|
id: "sonnet" | "opus";
|
||||||
name: string;
|
name: string;
|
||||||
@@ -100,8 +174,10 @@ const FloatingPromptInputInner = (
|
|||||||
) => {
|
) => {
|
||||||
const [prompt, setPrompt] = useState("");
|
const [prompt, setPrompt] = useState("");
|
||||||
const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel);
|
const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel);
|
||||||
|
const [selectedThinkingMode, setSelectedThinkingMode] = useState<ThinkingMode>("auto");
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
||||||
|
const [thinkingModePickerOpen, setThinkingModePickerOpen] = useState(false);
|
||||||
const [showFilePicker, setShowFilePicker] = useState(false);
|
const [showFilePicker, setShowFilePicker] = useState(false);
|
||||||
const [filePickerQuery, setFilePickerQuery] = useState("");
|
const [filePickerQuery, setFilePickerQuery] = useState("");
|
||||||
const [cursorPosition, setCursorPosition] = useState(0);
|
const [cursorPosition, setCursorPosition] = useState(0);
|
||||||
@@ -260,7 +336,15 @@ const FloatingPromptInputInner = (
|
|||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (prompt.trim() && !isLoading && !disabled) {
|
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("");
|
setPrompt("");
|
||||||
setEmbeddedImages([]);
|
setEmbeddedImages([]);
|
||||||
}
|
}
|
||||||
@@ -440,6 +524,7 @@ const FloatingPromptInputInner = (
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-muted-foreground">Model:</span>
|
<span className="text-xs text-muted-foreground">Model:</span>
|
||||||
<Button
|
<Button
|
||||||
@@ -453,6 +538,69 @@ const FloatingPromptInputInner = (
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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
|
<Button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={!prompt.trim() || isLoading || disabled}
|
disabled={!prompt.trim() || isLoading || disabled}
|
||||||
@@ -541,6 +689,66 @@ const FloatingPromptInputInner = (
|
|||||||
side="top"
|
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 */}
|
{/* Prompt Input */}
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<Textarea
|
<Textarea
|
||||||
|
Reference in New Issue
Block a user