Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
import { Brain, Check, ChevronDown } from 'lucide-react';
|
|
import React, { useRef } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { useI18n } from '../../application/i18n/I18nProvider';
|
|
import { formatComposerThinkingLabel } from '../../infrastructure/ai/composerPicker';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
|
|
|
export interface ComposerThinkingChipProps {
|
|
levels: readonly string[];
|
|
selectedLevel?: string;
|
|
disabled?: boolean;
|
|
open: boolean;
|
|
menuPos: { left: number; bottom: number } | null;
|
|
onToggle: (rect: DOMRect | undefined) => void;
|
|
onSelect: (level: string) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const chipClassName =
|
|
'inline-flex h-6 items-center gap-1 rounded-full px-1.5 text-[10.5px] text-foreground/72';
|
|
|
|
export const ComposerThinkingChip: React.FC<ComposerThinkingChipProps> = ({
|
|
levels,
|
|
selectedLevel,
|
|
disabled = false,
|
|
open,
|
|
menuPos,
|
|
onToggle,
|
|
onSelect,
|
|
onClose,
|
|
}) => {
|
|
const { t } = useI18n();
|
|
const btnRef = useRef<HTMLButtonElement>(null);
|
|
const formatLevel = (level: string) => (
|
|
level === 'off' ? t('ai.chat.thinkingOff') : formatComposerThinkingLabel(level)
|
|
);
|
|
const label = selectedLevel
|
|
? formatLevel(selectedLevel)
|
|
: t('ai.chat.thinkingLevel');
|
|
|
|
return (
|
|
<>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
ref={btnRef}
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={() => onToggle(btnRef.current?.getBoundingClientRect())}
|
|
className={`${chipClassName} shrink-0 ${
|
|
disabled
|
|
? 'opacity-60'
|
|
: 'cursor-pointer hover:bg-muted/24 transition-colors'
|
|
}`}
|
|
aria-label={t('ai.chat.thinkingLevel')}
|
|
aria-expanded={open}
|
|
>
|
|
<Brain size={11} className="text-violet-400/75" />
|
|
<span className="truncate max-w-[56px]">{label}</span>
|
|
{!disabled && <ChevronDown size={9} className="text-muted-foreground/50" />}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{t('ai.chat.thinkingLevel')}</TooltipContent>
|
|
</Tooltip>
|
|
{open && menuPos && createPortal(
|
|
<>
|
|
<div className="fixed inset-0 z-[999]" onClick={onClose} />
|
|
<div
|
|
role="listbox"
|
|
aria-label={t('ai.chat.thinkingLevel')}
|
|
className="fixed z-[1000] min-w-[148px] rounded-lg border border-border/50 bg-popover shadow-lg py-1"
|
|
style={{ left: menuPos.left, bottom: menuPos.bottom }}
|
|
>
|
|
{levels.map((level) => {
|
|
const isSelected = selectedLevel === level;
|
|
return (
|
|
<button
|
|
key={level}
|
|
type="button"
|
|
role="option"
|
|
aria-selected={isSelected}
|
|
onClick={() => {
|
|
if (!isSelected) onSelect(level);
|
|
else onClose();
|
|
}}
|
|
className="w-full flex items-center gap-2 px-2.5 py-1.5 text-left text-[12px] hover:bg-muted/30 transition-colors cursor-pointer"
|
|
>
|
|
{isSelected
|
|
? <Check size={11} className="text-primary shrink-0" />
|
|
: <span className="w-[11px] shrink-0" />}
|
|
<Brain size={12} className="text-violet-400/70 shrink-0" />
|
|
<span className="text-foreground/85">{formatLevel(level)}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</>,
|
|
document.body,
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default React.memo(ComposerThinkingChip);
|