mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-07 05:45:39 +08:00
Compare commits
1 Commits
features/r
...
chore/use-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a3c7f087a |
@@ -5,6 +5,9 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<title>CloudCLI UI</title>
|
<title>CloudCLI UI</title>
|
||||||
|
|
||||||
<!-- PWA Manifest -->
|
<!-- PWA Manifest -->
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const CLAUDE_MODELS = {
|
|||||||
{
|
{
|
||||||
value: "default",
|
value: "default",
|
||||||
label: "Default (recommended)",
|
label: "Default (recommended)",
|
||||||
description: "Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok",
|
description: "Use the default model (currently Opus 4.7 (1M context)) · $5/$25 per Mtok",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "sonnet",
|
value: "sonnet",
|
||||||
|
|||||||
@@ -31,24 +31,6 @@ export function createWebSocketServer(
|
|||||||
});
|
});
|
||||||
|
|
||||||
wss.on('connection', (ws, request) => {
|
wss.on('connection', (ws, request) => {
|
||||||
// Keep WebSocket alive across reverse-proxy idle timeouts (Cloudflare ~100s,
|
|
||||||
// AWS ALB 60s, nginx 60s, etc.). Without app-level pings these connections
|
|
||||||
// are silently torn down even when the UI is active, causing repeated
|
|
||||||
// reconnect cycles. ws library heartbeat is opt-in.
|
|
||||||
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
||||||
const heartbeat = setInterval(() => {
|
|
||||||
if (ws.readyState === ws.OPEN) {
|
|
||||||
try {
|
|
||||||
ws.ping();
|
|
||||||
} catch {
|
|
||||||
// socket may have been closed concurrently — interval will be cleared below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, HEARTBEAT_INTERVAL_MS);
|
|
||||||
const stopHeartbeat = () => clearInterval(heartbeat);
|
|
||||||
ws.on('close', stopHeartbeat);
|
|
||||||
ws.on('error', stopHeartbeat);
|
|
||||||
|
|
||||||
const incomingRequest = request as AuthenticatedWebSocketRequest;
|
const incomingRequest = request as AuthenticatedWebSocketRequest;
|
||||||
const url = incomingRequest.url ?? '/';
|
const url = incomingRequest.url ?? '/';
|
||||||
const pathname = new URL(url, 'http://localhost').pathname;
|
const pathname = new URL(url, 'http://localhost').pathname;
|
||||||
|
|||||||
44
src/components/chat/constants/thinkingModes.ts
Normal file
44
src/components/chat/constants/thinkingModes.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Brain, Zap, Sparkles, Atom } from 'lucide-react';
|
||||||
|
|
||||||
|
export const thinkingModes = [
|
||||||
|
{
|
||||||
|
id: 'none',
|
||||||
|
name: 'Standard',
|
||||||
|
description: 'Regular Claude response',
|
||||||
|
icon: null,
|
||||||
|
prefix: '',
|
||||||
|
color: 'text-gray-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'think',
|
||||||
|
name: 'Think',
|
||||||
|
description: 'Basic extended thinking',
|
||||||
|
icon: Brain,
|
||||||
|
prefix: 'think',
|
||||||
|
color: 'text-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'think-hard',
|
||||||
|
name: 'Think Hard',
|
||||||
|
description: 'More thorough evaluation',
|
||||||
|
icon: Zap,
|
||||||
|
prefix: 'think hard',
|
||||||
|
color: 'text-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'think-harder',
|
||||||
|
name: 'Think Harder',
|
||||||
|
description: 'Deep analysis with alternatives',
|
||||||
|
icon: Sparkles,
|
||||||
|
prefix: 'think harder',
|
||||||
|
color: 'text-indigo-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ultrathink',
|
||||||
|
name: 'Ultrathink',
|
||||||
|
description: 'Maximum thinking budget',
|
||||||
|
icon: Atom,
|
||||||
|
prefix: 'ultrathink',
|
||||||
|
color: 'text-red-600'
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
|
||||||
import { authenticatedFetch } from '../../../utils/api';
|
import { authenticatedFetch } from '../../../utils/api';
|
||||||
|
import { thinkingModes } from '../constants/thinkingModes';
|
||||||
import { grantClaudeToolPermission } from '../utils/chatPermissions';
|
import { grantClaudeToolPermission } from '../utils/chatPermissions';
|
||||||
import { safeLocalStorage } from '../utils/chatStorage';
|
import { safeLocalStorage } from '../utils/chatStorage';
|
||||||
import type {
|
import type {
|
||||||
@@ -203,6 +204,7 @@ export function useChatComposerState({
|
|||||||
const [uploadingImages, setUploadingImages] = useState<Map<string, number>>(new Map());
|
const [uploadingImages, setUploadingImages] = useState<Map<string, number>>(new Map());
|
||||||
const [imageErrors, setImageErrors] = useState<Map<string, string>>(new Map());
|
const [imageErrors, setImageErrors] = useState<Map<string, string>>(new Map());
|
||||||
const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
|
const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
|
||||||
|
const [thinkingMode, setThinkingMode] = useState('none');
|
||||||
const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null);
|
const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@@ -560,7 +562,11 @@ export function useChatComposerState({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageContent = currentInput;
|
let messageContent = currentInput;
|
||||||
|
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
|
||||||
|
if (selectedThinkingMode && selectedThinkingMode.prefix) {
|
||||||
|
messageContent = `${selectedThinkingMode.prefix}: ${currentInput}`;
|
||||||
|
}
|
||||||
|
|
||||||
let uploadedImages: unknown[] = [];
|
let uploadedImages: unknown[] = [];
|
||||||
if (attachedImages.length > 0) {
|
if (attachedImages.length > 0) {
|
||||||
@@ -743,6 +749,7 @@ export function useChatComposerState({
|
|||||||
setUploadingImages(new Map());
|
setUploadingImages(new Map());
|
||||||
setImageErrors(new Map());
|
setImageErrors(new Map());
|
||||||
setIsTextareaExpanded(false);
|
setIsTextareaExpanded(false);
|
||||||
|
setThinkingMode('none');
|
||||||
|
|
||||||
if (textareaRef.current) {
|
if (textareaRef.current) {
|
||||||
textareaRef.current.style.height = 'auto';
|
textareaRef.current.style.height = 'auto';
|
||||||
@@ -776,6 +783,7 @@ export function useChatComposerState({
|
|||||||
setIsLoading,
|
setIsLoading,
|
||||||
setIsUserScrolledUp,
|
setIsUserScrolledUp,
|
||||||
slashCommands,
|
slashCommands,
|
||||||
|
thinkingMode,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1012,6 +1020,8 @@ export function useChatComposerState({
|
|||||||
textareaRef,
|
textareaRef,
|
||||||
inputHighlightRef,
|
inputHighlightRef,
|
||||||
isTextareaExpanded,
|
isTextareaExpanded,
|
||||||
|
thinkingMode,
|
||||||
|
setThinkingMode,
|
||||||
slashCommandsCount,
|
slashCommandsCount,
|
||||||
filteredCommands,
|
filteredCommands,
|
||||||
frequentCommands,
|
frequentCommands,
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ function ChatInterface({
|
|||||||
textareaRef,
|
textareaRef,
|
||||||
inputHighlightRef,
|
inputHighlightRef,
|
||||||
isTextareaExpanded,
|
isTextareaExpanded,
|
||||||
|
thinkingMode,
|
||||||
|
setThinkingMode,
|
||||||
slashCommandsCount,
|
slashCommandsCount,
|
||||||
filteredCommands,
|
filteredCommands,
|
||||||
frequentCommands,
|
frequentCommands,
|
||||||
@@ -367,6 +369,8 @@ function ChatInterface({
|
|||||||
provider={provider}
|
provider={provider}
|
||||||
permissionMode={permissionMode}
|
permissionMode={permissionMode}
|
||||||
onModeSwitch={cyclePermissionMode}
|
onModeSwitch={cyclePermissionMode}
|
||||||
|
thinkingMode={thinkingMode}
|
||||||
|
setThinkingMode={setThinkingMode}
|
||||||
tokenBudget={tokenBudget}
|
tokenBudget={tokenBudget}
|
||||||
slashCommandsCount={slashCommandsCount}
|
slashCommandsCount={slashCommandsCount}
|
||||||
onToggleCommandMenu={handleToggleCommandMenu}
|
onToggleCommandMenu={handleToggleCommandMenu}
|
||||||
|
|||||||
@@ -2,16 +2,23 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import type {
|
import type {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
ClipboardEvent,
|
ClipboardEvent,
|
||||||
|
Dispatch,
|
||||||
FormEvent,
|
FormEvent,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
RefObject,
|
RefObject,
|
||||||
|
SetStateAction,
|
||||||
TouchEvent,
|
TouchEvent,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { ImageIcon, MessageSquareIcon, XIcon, ArrowDownIcon } from 'lucide-react';
|
import { ImageIcon, MessageSquareIcon, XIcon, ArrowDownIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types';
|
import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types';
|
||||||
|
import CommandMenu from './CommandMenu';
|
||||||
|
import ClaudeStatus from './ClaudeStatus';
|
||||||
|
import ImageAttachment from './ImageAttachment';
|
||||||
|
import PermissionRequestsBanner from './PermissionRequestsBanner';
|
||||||
|
import ThinkingModeSelector from './ThinkingModeSelector';
|
||||||
|
import TokenUsageSummary from './TokenUsageSummary';
|
||||||
import {
|
import {
|
||||||
PromptInput,
|
PromptInput,
|
||||||
PromptInputHeader,
|
PromptInputHeader,
|
||||||
@@ -23,12 +30,6 @@ import {
|
|||||||
PromptInputSubmit,
|
PromptInputSubmit,
|
||||||
} from '../../../../shared/view/ui';
|
} from '../../../../shared/view/ui';
|
||||||
|
|
||||||
import CommandMenu from './CommandMenu';
|
|
||||||
import ClaudeStatus from './ClaudeStatus';
|
|
||||||
import ImageAttachment from './ImageAttachment';
|
|
||||||
import PermissionRequestsBanner from './PermissionRequestsBanner';
|
|
||||||
import TokenUsageSummary from './TokenUsageSummary';
|
|
||||||
|
|
||||||
interface MentionableFile {
|
interface MentionableFile {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
@@ -57,6 +58,8 @@ interface ChatComposerProps {
|
|||||||
provider: Provider | string;
|
provider: Provider | string;
|
||||||
permissionMode: PermissionMode | string;
|
permissionMode: PermissionMode | string;
|
||||||
onModeSwitch: () => void;
|
onModeSwitch: () => void;
|
||||||
|
thinkingMode: string;
|
||||||
|
setThinkingMode: Dispatch<SetStateAction<string>>;
|
||||||
tokenBudget: Record<string, unknown> | null;
|
tokenBudget: Record<string, unknown> | null;
|
||||||
slashCommandsCount: number;
|
slashCommandsCount: number;
|
||||||
onToggleCommandMenu: () => void;
|
onToggleCommandMenu: () => void;
|
||||||
@@ -110,6 +113,8 @@ export default function ChatComposer({
|
|||||||
provider,
|
provider,
|
||||||
permissionMode,
|
permissionMode,
|
||||||
onModeSwitch,
|
onModeSwitch,
|
||||||
|
thinkingMode,
|
||||||
|
setThinkingMode,
|
||||||
tokenBudget,
|
tokenBudget,
|
||||||
slashCommandsCount,
|
slashCommandsCount,
|
||||||
onToggleCommandMenu,
|
onToggleCommandMenu,
|
||||||
@@ -290,7 +295,6 @@ export default function ChatComposer({
|
|||||||
|
|
||||||
<PromptInputTextarea
|
<PromptInputTextarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
dir="auto"
|
|
||||||
value={input}
|
value={input}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
onClick={onTextareaClick}
|
onClick={onTextareaClick}
|
||||||
@@ -353,6 +357,10 @@ export default function ChatComposer({
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{provider === 'claude' && (
|
||||||
|
<ThinkingModeSelector selectedMode={thinkingMode} onModeChange={setThinkingMode} onClose={() => {}} className="" />
|
||||||
|
)}
|
||||||
|
|
||||||
<TokenUsageSummary usage={tokenBudget} />
|
<TokenUsageSummary usage={tokenBudget} />
|
||||||
|
|
||||||
<PromptInputButton
|
<PromptInputButton
|
||||||
@@ -374,7 +382,7 @@ export default function ChatComposer({
|
|||||||
<PromptInputButton
|
<PromptInputButton
|
||||||
tooltip={{ content: t('input.clearInput', { defaultValue: 'Clear input' }) }}
|
tooltip={{ content: t('input.clearInput', { defaultValue: 'Clear input' }) }}
|
||||||
onClick={onClearInput}
|
onClick={onClearInput}
|
||||||
className="hidden sm:flex"
|
className="hidden sm:No-flex"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
@@ -391,8 +399,7 @@ export default function ChatComposer({
|
|||||||
{sendByCtrlEnter ? t('input.hintText.ctrlEnter') : t('input.hintText.enter')}
|
{sendByCtrlEnter ? t('input.hintText.ctrlEnter') : t('input.hintText.enter')}
|
||||||
</div>
|
</div>
|
||||||
<PromptInputSubmit
|
<PromptInputSubmit
|
||||||
onClick={isLoading ? onAbortSession : undefined}
|
disabled={!input.trim() || isLoading}
|
||||||
disabled={!isLoading && !input.trim()}
|
|
||||||
className="h-10 w-10 sm:h-10 sm:w-10"
|
className="h-10 w-10 sm:h-10 sm:w-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
|
|||||||
/* User message bubble on the right */
|
/* User message bubble on the right */
|
||||||
<div className="flex w-full items-end space-x-0 sm:w-auto sm:max-w-[85%] sm:space-x-3 md:max-w-md lg:max-w-lg xl:max-w-xl">
|
<div className="flex w-full items-end space-x-0 sm:w-auto sm:max-w-[85%] sm:space-x-3 md:max-w-md lg:max-w-lg xl:max-w-xl">
|
||||||
<div className="group flex-1 rounded-2xl rounded-br-md bg-blue-600 px-3 py-2 text-white shadow-sm sm:flex-initial sm:px-4">
|
<div className="group flex-1 rounded-2xl rounded-br-md bg-blue-600 px-3 py-2 text-white shadow-sm sm:flex-initial sm:px-4">
|
||||||
<div dir="auto" className="whitespace-pre-wrap break-words text-sm">
|
<div className="whitespace-pre-wrap break-words text-sm">
|
||||||
{message.content}
|
{message.content}
|
||||||
</div>
|
</div>
|
||||||
{message.images && message.images.length > 0 && (
|
{message.images && message.images.length > 0 && (
|
||||||
@@ -405,7 +405,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
|
|||||||
</ReasoningContent>
|
</ReasoningContent>
|
||||||
</Reasoning>
|
</Reasoning>
|
||||||
) : (
|
) : (
|
||||||
<div dir="auto" className="text-sm text-gray-700 dark:text-gray-300">
|
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
{/* Reasoning accordion */}
|
{/* Reasoning accordion */}
|
||||||
{showThinking && message.reasoning && (
|
{showThinking && message.reasoning && (
|
||||||
<Reasoning className="mb-3" defaultOpen={false}>
|
<Reasoning className="mb-3" defaultOpen={false}>
|
||||||
|
|||||||
244
src/components/chat/view/subcomponents/ThinkingModeSelector.tsx
Normal file
244
src/components/chat/view/subcomponents/ThinkingModeSelector.tsx
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import { useState, useRef, useEffect, useCallback, type CSSProperties } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { Brain, X } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { thinkingModes } from '../../constants/thinkingModes';
|
||||||
|
|
||||||
|
type ThinkingModeSelectorProps = {
|
||||||
|
selectedMode: string;
|
||||||
|
onModeChange: (modeId: string) => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className = '' }: ThinkingModeSelectorProps) {
|
||||||
|
const { t } = useTranslation('chat');
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties | null>(null);
|
||||||
|
|
||||||
|
// Mapping from mode ID to translation key
|
||||||
|
const modeKeyMap: Record<string, string> = {
|
||||||
|
'think-hard': 'thinkHard',
|
||||||
|
'think-harder': 'thinkHarder'
|
||||||
|
};
|
||||||
|
// Create translated modes for display
|
||||||
|
const translatedModes = thinkingModes.map(mode => {
|
||||||
|
const modeKey = modeKeyMap[mode.id] || mode.id;
|
||||||
|
return {
|
||||||
|
...mode,
|
||||||
|
name: t(`thinkingMode.modes.${modeKey}.name`),
|
||||||
|
description: t(`thinkingMode.modes.${modeKey}.description`),
|
||||||
|
prefix: t(`thinkingMode.modes.${modeKey}.prefix`)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeDropdown = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
onClose?.();
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const updateDropdownPosition = useCallback(() => {
|
||||||
|
const trigger = triggerRef.current;
|
||||||
|
const dropdown = dropdownRef.current;
|
||||||
|
if (!trigger || !dropdown || typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerRect = trigger.getBoundingClientRect();
|
||||||
|
const viewportPadding = window.innerWidth < 640 ? 12 : 16;
|
||||||
|
const spacing = 8;
|
||||||
|
const width = Math.min(window.innerWidth - viewportPadding * 2, window.innerWidth < 640 ? 320 : 256);
|
||||||
|
let left = triggerRect.left + triggerRect.width / 2 - width / 2;
|
||||||
|
left = Math.max(viewportPadding, Math.min(left, window.innerWidth - width - viewportPadding));
|
||||||
|
|
||||||
|
const measuredHeight = dropdown.offsetHeight || 0;
|
||||||
|
const spaceBelow = window.innerHeight - triggerRect.bottom - spacing - viewportPadding;
|
||||||
|
const spaceAbove = triggerRect.top - spacing - viewportPadding;
|
||||||
|
const openBelow = spaceBelow >= Math.min(measuredHeight || 320, 320) || spaceBelow >= spaceAbove;
|
||||||
|
const availableHeight = Math.min(
|
||||||
|
window.innerHeight - viewportPadding * 2,
|
||||||
|
Math.max(180, openBelow ? spaceBelow : spaceAbove),
|
||||||
|
);
|
||||||
|
const panelHeight = Math.min(measuredHeight || availableHeight, availableHeight);
|
||||||
|
const top = openBelow
|
||||||
|
? Math.min(triggerRect.bottom + spacing, window.innerHeight - viewportPadding - panelHeight)
|
||||||
|
: Math.max(viewportPadding, triggerRect.top - spacing - panelHeight);
|
||||||
|
|
||||||
|
setDropdownStyle({
|
||||||
|
position: 'fixed',
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
width,
|
||||||
|
maxHeight: availableHeight,
|
||||||
|
zIndex: 80,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setDropdownStyle(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rafId = window.requestAnimationFrame(updateDropdownPosition);
|
||||||
|
const handleViewportChange = () => updateDropdownPosition();
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleViewportChange);
|
||||||
|
window.addEventListener('scroll', handleViewportChange, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(rafId);
|
||||||
|
window.removeEventListener('resize', handleViewportChange);
|
||||||
|
window.removeEventListener('scroll', handleViewportChange, true);
|
||||||
|
};
|
||||||
|
}, [isOpen, updateDropdownPosition]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerRef.current?.contains(target) || dropdownRef.current?.contains(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDropdown();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('pointerdown', handlePointerDown, true);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('pointerdown', handlePointerDown, true);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [isOpen, closeDropdown]);
|
||||||
|
|
||||||
|
const currentMode = translatedModes.find(mode => mode.id === selectedMode) || translatedModes[0];
|
||||||
|
const IconComponent = currentMode.icon || Brain;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative ${className}`} ref={containerRef}>
|
||||||
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (isOpen) {
|
||||||
|
closeDropdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
className={`flex h-10 w-10 items-center justify-center rounded-full transition-all duration-200 sm:h-10 sm:w-10 ${selectedMode === 'none'
|
||||||
|
? 'bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600'
|
||||||
|
: 'bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800'
|
||||||
|
}`}
|
||||||
|
title={t('thinkingMode.buttonTitle', { mode: currentMode.name })}
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
>
|
||||||
|
<IconComponent className={`h-5 w-5 ${currentMode.color}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && typeof document !== 'undefined' && createPortal(
|
||||||
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
|
style={dropdownStyle || { position: 'fixed', top: 0, left: 0, visibility: 'hidden' }}
|
||||||
|
className="flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="false"
|
||||||
|
>
|
||||||
|
<div className="border-b border-gray-200 p-3 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
|
{t('thinkingMode.selector.title')}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={closeDropdown}
|
||||||
|
className="rounded p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{t('thinkingMode.selector.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-h-0 overflow-y-auto py-1">
|
||||||
|
{translatedModes.map((mode) => {
|
||||||
|
const ModeIcon = mode.icon;
|
||||||
|
const isSelected = mode.id === selectedMode;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={mode.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onModeChange(mode.id);
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
className={`w-full px-4 py-3 text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-700 ${isSelected ? 'bg-gray-50 dark:bg-gray-700' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className={`mt-0.5 ${mode.icon ? mode.color : 'text-gray-400'}`}>
|
||||||
|
{ModeIcon ? <ModeIcon className="h-5 w-5" /> : <div className="h-5 w-5" />}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`text-sm font-medium ${isSelected ? 'text-gray-900 dark:text-white' : 'text-gray-700 dark:text-gray-300'
|
||||||
|
}`}>
|
||||||
|
{mode.name}
|
||||||
|
</span>
|
||||||
|
{isSelected && (
|
||||||
|
<span className="rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-700 dark:bg-blue-900 dark:text-blue-300">
|
||||||
|
{t('thinkingMode.selector.active')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{mode.description}
|
||||||
|
</p>
|
||||||
|
{mode.prefix && (
|
||||||
|
<code className="mt-1 inline-block rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-700">
|
||||||
|
{mode.prefix}
|
||||||
|
</code>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<strong>Tip:</strong> {t('thinkingMode.selector.tip')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThinkingModeSelector;
|
||||||
@@ -36,10 +36,6 @@ const useWebSocketProviderState = (): WebSocketContextType => {
|
|||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// The cleanup below sets unmountedRef = true. Without this reset, every
|
|
||||||
// re-run of the effect (e.g. on token refresh) would short-circuit connect()
|
|
||||||
// at its unmounted guard and leave the socket permanently disconnected.
|
|
||||||
unmountedRef.current = false;
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -138,6 +138,42 @@
|
|||||||
"clearInput": "Eingabe leeren",
|
"clearInput": "Eingabe leeren",
|
||||||
"scrollToBottom": "Nach unten scrollen"
|
"scrollToBottom": "Nach unten scrollen"
|
||||||
},
|
},
|
||||||
|
"thinkingMode": {
|
||||||
|
"selector": {
|
||||||
|
"title": "Denkmodus",
|
||||||
|
"description": "Erweitertes Denken gibt Claude mehr Zeit, Alternativen zu evaluieren",
|
||||||
|
"active": "Aktiv",
|
||||||
|
"tip": "Höhere Denkmodi brauchen mehr Zeit, liefern aber eine gründlichere Analyse"
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"none": {
|
||||||
|
"name": "Standard",
|
||||||
|
"description": "Reguläre Claude-Antwort",
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"think": {
|
||||||
|
"name": "Denken",
|
||||||
|
"description": "Grundlegendes erweitertes Denken",
|
||||||
|
"prefix": "think"
|
||||||
|
},
|
||||||
|
"thinkHard": {
|
||||||
|
"name": "Intensiv denken",
|
||||||
|
"description": "Gründlichere Auswertung",
|
||||||
|
"prefix": "think hard"
|
||||||
|
},
|
||||||
|
"thinkHarder": {
|
||||||
|
"name": "Sehr intensiv denken",
|
||||||
|
"description": "Tiefgehende Analyse mit Alternativen",
|
||||||
|
"prefix": "think harder"
|
||||||
|
},
|
||||||
|
"ultrathink": {
|
||||||
|
"name": "Ultradenken",
|
||||||
|
"description": "Maximales Denkbudget",
|
||||||
|
"prefix": "ultrathink"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttonTitle": "Denkmodus: {{mode}}"
|
||||||
|
},
|
||||||
"providerSelection": {
|
"providerSelection": {
|
||||||
"title": "KI-Assistent wählen",
|
"title": "KI-Assistent wählen",
|
||||||
"description": "Anbieter auswählen, um eine neue Unterhaltung zu starten",
|
"description": "Anbieter auswählen, um eine neue Unterhaltung zu starten",
|
||||||
|
|||||||
@@ -139,6 +139,42 @@
|
|||||||
"clearInput": "Clear input",
|
"clearInput": "Clear input",
|
||||||
"scrollToBottom": "Scroll to bottom"
|
"scrollToBottom": "Scroll to bottom"
|
||||||
},
|
},
|
||||||
|
"thinkingMode": {
|
||||||
|
"selector": {
|
||||||
|
"title": "Thinking Mode",
|
||||||
|
"description": "Extended thinking gives Claude more time to evaluate alternatives",
|
||||||
|
"active": "Active",
|
||||||
|
"tip": "Higher thinking modes take more time but provide more thorough analysis"
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"none": {
|
||||||
|
"name": "Standard",
|
||||||
|
"description": "Regular Claude response",
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"think": {
|
||||||
|
"name": "Think",
|
||||||
|
"description": "Basic extended thinking",
|
||||||
|
"prefix": "think"
|
||||||
|
},
|
||||||
|
"thinkHard": {
|
||||||
|
"name": "Think Hard",
|
||||||
|
"description": "More thorough evaluation",
|
||||||
|
"prefix": "think hard"
|
||||||
|
},
|
||||||
|
"thinkHarder": {
|
||||||
|
"name": "Think Harder",
|
||||||
|
"description": "Deep analysis with alternatives",
|
||||||
|
"prefix": "think harder"
|
||||||
|
},
|
||||||
|
"ultrathink": {
|
||||||
|
"name": "Ultrathink",
|
||||||
|
"description": "Maximum thinking budget",
|
||||||
|
"prefix": "ultrathink"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttonTitle": "Thinking mode: {{mode}}"
|
||||||
|
},
|
||||||
"providerSelection": {
|
"providerSelection": {
|
||||||
"title": "Choose Your AI Assistant",
|
"title": "Choose Your AI Assistant",
|
||||||
"description": "Select a provider to start a new conversation",
|
"description": "Select a provider to start a new conversation",
|
||||||
|
|||||||
@@ -138,6 +138,42 @@
|
|||||||
"clearInput": "Cancella input",
|
"clearInput": "Cancella input",
|
||||||
"scrollToBottom": "Scorri in basso"
|
"scrollToBottom": "Scorri in basso"
|
||||||
},
|
},
|
||||||
|
"thinkingMode": {
|
||||||
|
"selector": {
|
||||||
|
"title": "Modalità ragionamento",
|
||||||
|
"description": "Il ragionamento esteso dà a Claude più tempo per valutare le alternative",
|
||||||
|
"active": "Attivo",
|
||||||
|
"tip": "Modalità di ragionamento più elevate richiedono più tempo ma forniscono un'analisi più approfondita"
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"none": {
|
||||||
|
"name": "Standard",
|
||||||
|
"description": "Risposta Claude normale",
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"think": {
|
||||||
|
"name": "Pensa",
|
||||||
|
"description": "Ragionamento esteso base",
|
||||||
|
"prefix": "think"
|
||||||
|
},
|
||||||
|
"thinkHard": {
|
||||||
|
"name": "Pensa di più",
|
||||||
|
"description": "Valutazione più approfondita",
|
||||||
|
"prefix": "think hard"
|
||||||
|
},
|
||||||
|
"thinkHarder": {
|
||||||
|
"name": "Pensa ancora",
|
||||||
|
"description": "Analisi profonda con alternative",
|
||||||
|
"prefix": "think harder"
|
||||||
|
},
|
||||||
|
"ultrathink": {
|
||||||
|
"name": "Ultrapensiero",
|
||||||
|
"description": "Budget massimo di ragionamento",
|
||||||
|
"prefix": "ultrathink"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttonTitle": "Modalità ragionamento: {{mode}}"
|
||||||
|
},
|
||||||
"providerSelection": {
|
"providerSelection": {
|
||||||
"title": "Scegli il tuo assistente AI",
|
"title": "Scegli il tuo assistente AI",
|
||||||
"description": "Seleziona un provider per iniziare una nuova conversazione",
|
"description": "Seleziona un provider per iniziare una nuova conversazione",
|
||||||
|
|||||||
@@ -117,6 +117,42 @@
|
|||||||
"clickToChangeMode": "クリックで権限モードを変更(または入力欄でTab)",
|
"clickToChangeMode": "クリックで権限モードを変更(または入力欄でTab)",
|
||||||
"showAllCommands": "すべてのコマンドを表示"
|
"showAllCommands": "すべてのコマンドを表示"
|
||||||
},
|
},
|
||||||
|
"thinkingMode": {
|
||||||
|
"selector": {
|
||||||
|
"title": "思考モード",
|
||||||
|
"description": "拡張思考によりClaudeがより多くの選択肢を検討できます",
|
||||||
|
"active": "有効",
|
||||||
|
"tip": "高い思考モードは時間がかかりますが、より深い分析が得られます"
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"none": {
|
||||||
|
"name": "標準",
|
||||||
|
"description": "通常のClaudeの応答",
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"think": {
|
||||||
|
"name": "Think",
|
||||||
|
"description": "基本的な拡張思考",
|
||||||
|
"prefix": "think"
|
||||||
|
},
|
||||||
|
"thinkHard": {
|
||||||
|
"name": "Think Hard",
|
||||||
|
"description": "より深い検討",
|
||||||
|
"prefix": "think hard"
|
||||||
|
},
|
||||||
|
"thinkHarder": {
|
||||||
|
"name": "Think Harder",
|
||||||
|
"description": "代替案を含む深い分析",
|
||||||
|
"prefix": "think harder"
|
||||||
|
},
|
||||||
|
"ultrathink": {
|
||||||
|
"name": "Ultrathink",
|
||||||
|
"description": "最大限の思考予算",
|
||||||
|
"prefix": "ultrathink"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttonTitle": "思考モード: {{mode}}"
|
||||||
|
},
|
||||||
"providerSelection": {
|
"providerSelection": {
|
||||||
"title": "AIアシスタントを選択",
|
"title": "AIアシスタントを選択",
|
||||||
"description": "新しい会話を始めるプロバイダーを選択してください",
|
"description": "新しい会話を始めるプロバイダーを選択してください",
|
||||||
|
|||||||
@@ -120,6 +120,42 @@
|
|||||||
"clearInput": "입력 지우기",
|
"clearInput": "입력 지우기",
|
||||||
"scrollToBottom": "맨 아래로 스크롤"
|
"scrollToBottom": "맨 아래로 스크롤"
|
||||||
},
|
},
|
||||||
|
"thinkingMode": {
|
||||||
|
"selector": {
|
||||||
|
"title": "Thinking 모드",
|
||||||
|
"description": "확장된 thinking은 Claude에게 대안을 평가할 시간을 더 줍니다",
|
||||||
|
"active": "활성",
|
||||||
|
"tip": "높은 thinking 모드는 시간이 더 걸리지만 더 철저한 분석을 제공합니다"
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"none": {
|
||||||
|
"name": "Standard",
|
||||||
|
"description": "일반 Claude 응답",
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"think": {
|
||||||
|
"name": "Think",
|
||||||
|
"description": "기본 확장 thinking",
|
||||||
|
"prefix": "think"
|
||||||
|
},
|
||||||
|
"thinkHard": {
|
||||||
|
"name": "Think Hard",
|
||||||
|
"description": "더 철저한 평가",
|
||||||
|
"prefix": "think hard"
|
||||||
|
},
|
||||||
|
"thinkHarder": {
|
||||||
|
"name": "Think Harder",
|
||||||
|
"description": "대안을 포함한 심층 분석",
|
||||||
|
"prefix": "think harder"
|
||||||
|
},
|
||||||
|
"ultrathink": {
|
||||||
|
"name": "Ultrathink",
|
||||||
|
"description": "최대 thinking 예산",
|
||||||
|
"prefix": "ultrathink"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttonTitle": "Thinking 모드: {{mode}}"
|
||||||
|
},
|
||||||
"providerSelection": {
|
"providerSelection": {
|
||||||
"title": "AI 어시스턴트 선택",
|
"title": "AI 어시스턴트 선택",
|
||||||
"description": "새 대화를 시작할 프로바이더를 선택하세요",
|
"description": "새 대화를 시작할 프로바이더를 선택하세요",
|
||||||
|
|||||||
@@ -138,6 +138,42 @@
|
|||||||
"clearInput": "Очистить ввод",
|
"clearInput": "Очистить ввод",
|
||||||
"scrollToBottom": "Прокрутить вниз"
|
"scrollToBottom": "Прокрутить вниз"
|
||||||
},
|
},
|
||||||
|
"thinkingMode": {
|
||||||
|
"selector": {
|
||||||
|
"title": "Режим размышления",
|
||||||
|
"description": "Расширенное размышление дает Claude больше времени для оценки альтернатив",
|
||||||
|
"active": "Активен",
|
||||||
|
"tip": "Более высокие режимы размышления занимают больше времени, но обеспечивают более тщательный анализ"
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"none": {
|
||||||
|
"name": "Стандартный",
|
||||||
|
"description": "Обычный ответ Claude",
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"think": {
|
||||||
|
"name": "Думать",
|
||||||
|
"description": "Базовое расширенное размышление",
|
||||||
|
"prefix": "думать"
|
||||||
|
},
|
||||||
|
"thinkHard": {
|
||||||
|
"name": "Думать усердно",
|
||||||
|
"description": "Более тщательная оценка",
|
||||||
|
"prefix": "думать усердно"
|
||||||
|
},
|
||||||
|
"thinkHarder": {
|
||||||
|
"name": "Думать еще усерднее",
|
||||||
|
"description": "Глубокий анализ с альтернативами",
|
||||||
|
"prefix": "думать еще усерднее"
|
||||||
|
},
|
||||||
|
"ultrathink": {
|
||||||
|
"name": "Ультра-размышление",
|
||||||
|
"description": "Максимальный бюджет размышления",
|
||||||
|
"prefix": "ультра-размышление"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttonTitle": "Режим размышления: {{mode}}"
|
||||||
|
},
|
||||||
"providerSelection": {
|
"providerSelection": {
|
||||||
"title": "Выберите вашего AI-ассистента",
|
"title": "Выберите вашего AI-ассистента",
|
||||||
"description": "Выберите провайдера для начала нового разговора",
|
"description": "Выберите провайдера для начала нового разговора",
|
||||||
|
|||||||
@@ -138,6 +138,42 @@
|
|||||||
"clearInput": "Girdiyi temizle",
|
"clearInput": "Girdiyi temizle",
|
||||||
"scrollToBottom": "En alta git"
|
"scrollToBottom": "En alta git"
|
||||||
},
|
},
|
||||||
|
"thinkingMode": {
|
||||||
|
"selector": {
|
||||||
|
"title": "Düşünme Modu",
|
||||||
|
"description": "Uzatılmış düşünme, Claude'a alternatifleri değerlendirmek için daha fazla zaman verir",
|
||||||
|
"active": "Aktif",
|
||||||
|
"tip": "Daha yüksek düşünme modları daha fazla zaman alır ama daha kapsamlı analiz sağlar"
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"none": {
|
||||||
|
"name": "Standart",
|
||||||
|
"description": "Normal Claude yanıtı",
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"think": {
|
||||||
|
"name": "Düşün",
|
||||||
|
"description": "Temel uzatılmış düşünme",
|
||||||
|
"prefix": "think"
|
||||||
|
},
|
||||||
|
"thinkHard": {
|
||||||
|
"name": "Daha Fazla Düşün",
|
||||||
|
"description": "Daha kapsamlı değerlendirme",
|
||||||
|
"prefix": "think hard"
|
||||||
|
},
|
||||||
|
"thinkHarder": {
|
||||||
|
"name": "Derin Düşün",
|
||||||
|
"description": "Alternatiflerle derin analiz",
|
||||||
|
"prefix": "think harder"
|
||||||
|
},
|
||||||
|
"ultrathink": {
|
||||||
|
"name": "Ultra Düşün",
|
||||||
|
"description": "Maksimum düşünme bütçesi",
|
||||||
|
"prefix": "ultrathink"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttonTitle": "Düşünme modu: {{mode}}"
|
||||||
|
},
|
||||||
"providerSelection": {
|
"providerSelection": {
|
||||||
"title": "AI Asistanını Seç",
|
"title": "AI Asistanını Seç",
|
||||||
"description": "Yeni bir konuşma başlatmak için bir sağlayıcı seç",
|
"description": "Yeni bir konuşma başlatmak için bir sağlayıcı seç",
|
||||||
|
|||||||
@@ -120,6 +120,42 @@
|
|||||||
"clearInput": "清空输入",
|
"clearInput": "清空输入",
|
||||||
"scrollToBottom": "滚动到底部"
|
"scrollToBottom": "滚动到底部"
|
||||||
},
|
},
|
||||||
|
"thinkingMode": {
|
||||||
|
"selector": {
|
||||||
|
"title": "思考模式",
|
||||||
|
"description": "扩展思考给 Claude 更多时间来评估替代方案",
|
||||||
|
"active": "激活",
|
||||||
|
"tip": "更高的思考模式需要更多时间,但提供更彻底的分析"
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"none": {
|
||||||
|
"name": "标准",
|
||||||
|
"description": "常规 Claude 响应",
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"think": {
|
||||||
|
"name": "思考",
|
||||||
|
"description": "基本扩展思考",
|
||||||
|
"prefix": "思考"
|
||||||
|
},
|
||||||
|
"thinkHard": {
|
||||||
|
"name": "深入思考",
|
||||||
|
"description": "更彻底的评估",
|
||||||
|
"prefix": "深入思考"
|
||||||
|
},
|
||||||
|
"thinkHarder": {
|
||||||
|
"name": "更深入思考",
|
||||||
|
"description": "考虑替代方案的深度分析",
|
||||||
|
"prefix": "更深入思考"
|
||||||
|
},
|
||||||
|
"ultrathink": {
|
||||||
|
"name": "超级思考",
|
||||||
|
"description": "最大思考预算",
|
||||||
|
"prefix": "超级思考"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttonTitle": "思考模式:{{mode}}"
|
||||||
|
},
|
||||||
"providerSelection": {
|
"providerSelection": {
|
||||||
"title": "选择您的 AI 助手",
|
"title": "选择您的 AI 助手",
|
||||||
"description": "选择一个供应商以开始新对话",
|
"description": "选择一个供应商以开始新对话",
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
'/shell': {
|
'/shell': {
|
||||||
target: `ws://${proxyHost}:${serverPort}`,
|
target: `ws://${proxyHost}:${serverPort}`,
|
||||||
ws: true
|
ws: true
|
||||||
},
|
|
||||||
'/plugin-ws': {
|
|
||||||
target: `ws://${proxyHost}:${serverPort}`,
|
|
||||||
ws: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user