diff --git a/.env.development b/.env.development index 76a7db8e4..baba06b6a 100644 --- a/.env.development +++ b/.env.development @@ -10,6 +10,16 @@ REMOTE_CONTROL_WEB_ORIGIN=https://remote.eigent.ai VITE_REMOTE_CONTROL_LOCAL_API_URL=https://dev.eigent.ai # =======Production environment variables end ======= +# =======Test environment variables start ======= +# SERVER_URL=https://test-dev.eigent.ai +# VITE_PROXY_URL=https://test-dev.eigent.ai +# VITE_SITE_URL=https://test.eigent.ai +# VITE_USE_LOCAL_PROXY=false +# VITE_REMOTE_CONTROL_WEB_ORIGIN=https://test-remote.eigent.ai +# REMOTE_CONTROL_WEB_ORIGIN=https://test-remote.eigent.ai +# VITE_REMOTE_CONTROL_LOCAL_API_URL=https://test-dev.eigent.ai +# =======Test environment variables end======= + # =======Local environment variables start ======= # SERVER_URL=http://localhost:3001 # VITE_PROXY_URL=http://localhost:3001 @@ -23,4 +33,4 @@ VITE_REMOTE_CONTROL_LOCAL_API_URL=https://dev.eigent.ai # Replace with real values from Stack dashboard when needed. VITE_STACK_PROJECT_ID=dummy_project_id VITE_STACK_PUBLISHABLE_CLIENT_KEY=dummy_publishable_key -VITE_STACK_SECRET_SERVER_KEY=dummy_secret_server_key +VITE_STACK_SECRET_SERVER_KEY=dummy_secret_server_key \ No newline at end of file diff --git a/src/components/ChatBox/BottomBox/ApprovalModeSelect.tsx b/src/components/ChatBox/BottomBox/ApprovalModeSelect.tsx new file mode 100644 index 000000000..7bbb0097d --- /dev/null +++ b/src/components/ChatBox/BottomBox/ApprovalModeSelect.tsx @@ -0,0 +1,173 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +/** + * Approval-mode picker for the chat input bar — same pill-trigger shell as + * `ThinkingEffortSelect` / `ModelSelect` / `ProjectModeToggle` so the + * controls read as one family in the `BoxFooter` row. + * + * UI only for now: it holds its own local state and is not wired to the + * human-toolkit / approval backend. + */ + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { cn } from '@/lib/utils'; +import { Check, ChevronDown, ShieldCheck, TriangleAlert } from 'lucide-react'; +import { useState } from 'react'; + +export type ApprovalMode = 'manual' | 'skip'; + +interface ApprovalOption { + value: ApprovalMode; + label: string; + icon: typeof ShieldCheck; +} + +const APPROVAL_OPTIONS: ApprovalOption[] = [ + { value: 'manual', label: 'Manual Approval', icon: ShieldCheck }, + { value: 'skip', label: 'Skip all approval', icon: TriangleAlert }, +]; + +// Keep in sync with `DropdownMenuContent`'s `w-[180px]` below. +const MENU_CONTENT_WIDTH_CLASS = 'w-[180px]'; + +const triggerShellClass = cn( + 'rounded-xl px-2 py-1 inline-flex max-w-[min(100%,320px)] shrink-0 items-center gap-1.5', + 'bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default' +); + +export interface ApprovalModeSelectProps { + disabled?: boolean; + /** Shows the current mode in the same read-only shell as the model/mode controls. */ + readOnly?: boolean; + /** When true, hides the text label and shows only the icon (narrow footer). */ + compact?: boolean; + className?: string; +} + +export function ApprovalModeSelect({ + disabled, + readOnly = false, + compact = false, + className, +}: ApprovalModeSelectProps) { + const [value, setValue] = useState('manual'); + + const current = + APPROVAL_OPTIONS.find((o) => o.value === value) ?? APPROVAL_OPTIONS[0]; + const CurrentIcon = current.icon; + + if (readOnly) { + return ( +
+ + + {!compact && ( + + {current.label} + + )} + +
+ ); + } + + return ( + + + + + + {APPROVAL_OPTIONS.map((option) => { + const OptionIcon = option.icon; + return ( + setValue(option.value)} + className="flex items-center justify-between gap-2" + > + + + {option.label} + + {value === option.value && ( + + )} + + ); + })} + + + ); +} diff --git a/src/components/ChatBox/BottomBox/BoxAction.tsx b/src/components/ChatBox/BottomBox/BoxAction.tsx deleted file mode 100644 index fe08487ab..000000000 --- a/src/components/ChatBox/BottomBox/BoxAction.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { useTranslation } from 'react-i18next'; - -interface BoxActionProps { - /** Task status for determining what button to show */ - status?: 'running' | 'finished' | 'pending' | 'pause'; - /** Task time display */ - taskTime?: string; - /** Callback for pause/resume */ - onPauseResume?: () => void; - /** Loading state for pause/resume */ - pauseResumeLoading?: boolean; - className?: string; -} - -export function BoxAction({ - status: _status, - taskTime: _taskTime, - onPauseResume: _onPauseResume, - pauseResumeLoading: _pauseResumeLoading = false, - className, -}: BoxActionProps) { - const { t } = useTranslation(); - - return ( -
- {/* Placeholder for future actions */} -
-
- ); -} diff --git a/src/components/ChatBox/BottomBox/BoxFooter.tsx b/src/components/ChatBox/BottomBox/BoxFooter.tsx new file mode 100644 index 000000000..3777fd118 --- /dev/null +++ b/src/components/ChatBox/BottomBox/BoxFooter.tsx @@ -0,0 +1,93 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { ProjectModeToggle } from '@/components/Workspace/ProjectModeToggle'; +import { useIsCompactWidth } from '@/hooks/useIsCompactWidth'; +import type { SessionModeType, ThinkingEffortType } from '@/types/constants'; +import { ApprovalModeSelect } from './ApprovalModeSelect'; +import { ModelSelect } from './ModelSelect'; +import { ThinkingEffortSelect } from './ThinkingEffortSelect'; + +/** + * Below this footer width the left-side controls (session mode + approval mode) + * collapse to icon-only so everything stays on a single row. + */ +const COMPACT_WIDTH_THRESHOLD = 460; + +export interface BoxFooterProps { + /** Left side: single-agent / multi-agent mode control. */ + sessionMode: SessionModeType; + onSessionModeChange?: (mode: SessionModeType) => void; + /** Reasoning effort control; omit to hide it from the row. */ + thinkingEffort?: ThinkingEffortType; + onThinkingEffortChange?: (effort: ThinkingEffortType) => void; + /** + * Project-setup controls: interactive on the workspace composer only. + * Once the project has started both controls render read-only. + */ + interactive?: boolean; + disabled?: boolean; +} + +/** + * BoxFooter — project-setup row under BoxMain in the BottomBox shell. + * Left: session mode + approval mode controls. Right: default model + thinking + * effort controls. Stays on a single row; the left controls collapse to + * icon-only when the footer gets narrow. + */ +export function BoxFooter({ + sessionMode, + onSessionModeChange, + thinkingEffort, + onThinkingEffortChange, + interactive = false, + disabled = false, +}: BoxFooterProps) { + const [footerRef, compact] = useIsCompactWidth( + COMPACT_WIDTH_THRESHOLD + ); + + return ( +
+
+ {})} + readOnly={!interactive} + compact={compact} + className="shrink-0" + /> + +
+
+ + {thinkingEffort !== undefined && ( + + )} +
+
+ ); +} diff --git a/src/components/ChatBox/BottomBox/InputBox.tsx b/src/components/ChatBox/BottomBox/InputBox.tsx index c6b99ba32..a65b3dba4 100644 --- a/src/components/ChatBox/BottomBox/InputBox.tsx +++ b/src/components/ChatBox/BottomBox/InputBox.tsx @@ -13,35 +13,27 @@ // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; -import { ProjectModeToggle } from '@/components/Workspace/ProjectModeToggle'; import { processDroppedFiles } from '@/lib/fileUtils'; import { cn } from '@/lib/utils'; import type { TriggerInput } from '@/types'; -import type { SessionModeType } from '@/types/constants'; import { ArrowRight, FileText, + Hammer, Image, Paperclip, - Plus, UploadCloud, + WandSparkles, X, } from 'lucide-react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; -import { ChatInputModelDropdown } from './ChatInputModelDropdown'; import { RichChatInput } from './RichChatInput'; /** @@ -86,12 +78,12 @@ export interface InputboxProps { privacy?: boolean; /** Use cloud model in dev */ useCloudModelInDev?: boolean; - /** Session mode for the mode-select row; omit to hide it. */ - sessionMode?: SessionModeType; - /** Called when the user changes mode (workspace only). */ - onSessionModeChange?: (mode: SessionModeType) => void; - /** Full toggle on workspace; on session chat, only the current mode is shown. */ - sessionModeSelectInteractive?: boolean; + /** Connector picker panel state; the toggle button only renders when the callback is provided. */ + connectorPanelOpen?: boolean; + onToggleConnectorPanel?: () => void; + /** Skill picker panel state; the toggle button only renders when the callback is provided. */ + skillPanelOpen?: boolean; + onToggleSkillPanel?: () => void; /** Callback when trigger is being created (for placeholder) */ onTriggerCreating?: (triggerData: TriggerInput) => void; /** Callback when trigger is created successfully */ @@ -152,9 +144,10 @@ export const Inputbox = ({ allowDragDrop = false, privacy = true, useCloudModelInDev = false, - sessionMode, - onSessionModeChange, - sessionModeSelectInteractive = false, + connectorPanelOpen = false, + onToggleConnectorPanel, + skillPanelOpen = false, + onToggleSkillPanel, onTriggerCreating: _onTriggerCreating, onTriggerCreated: _onTriggerCreated, }: InputboxProps) => { @@ -310,7 +303,7 @@ export const Inputbox = ({ return (
{isDragging && ( -
+
{t('chat.drop-files-to-attach')} @@ -332,14 +325,14 @@ export const Inputbox = ({ )} {/* Layer 2: File attachments (only show if has files) */} {files.length > 0 && ( -
+
{visibleFiles.map((file) => { const isHovered = hoveredFilePath === file.filePath; return (
setHoveredFilePath(file.filePath)} onMouseLeave={() => @@ -352,7 +345,7 @@ export const Inputbox = ({ { e.preventDefault(); @@ -371,7 +364,7 @@ export const Inputbox = ({ {/* File Name */}

@@ -390,14 +383,14 @@ export const Inputbox = ({ buttonContent="text" textWeight="bold" buttonRadius="full" - className="rounded-lg bg-ds-bg-neutral-strong-default relative box-border flex h-auto items-center" + className="relative box-border flex h-auto items-center rounded-lg bg-ds-bg-neutral-strong-default" onMouseEnter={openRemainingPopover} onMouseLeave={scheduleCloseRemainingPopover} onClick={(e) => { e.stopPropagation(); }} > -

+

{remainingCount}+

@@ -406,17 +399,17 @@ export const Inputbox = ({ align="end" side="right" sideOffset={4} - className="max-w-40 rounded-lg border-ds-border-neutral-subtle-default bg-ds-bg-neutral-default-default p-1 shadow-perfect !w-auto border-solid" + className="!w-auto max-w-40 rounded-lg border-solid border-ds-border-neutral-subtle-default bg-ds-bg-neutral-default-default p-1 shadow-perfect" onMouseEnter={openRemainingPopover} onMouseLeave={scheduleCloseRemainingPopover} > -
+
{files.slice(maxVisibleFiles).map((file) => { const isHovered = hoveredFilePath === file.filePath; return ( @@ -459,7 +452,7 @@ export const Inputbox = ({ )} {/* Layer 3: Text input area */} -
+
} value={value} @@ -489,67 +482,77 @@ export const Inputbox = ({
{/* Layer 4: Action buttons */} -
- {/* Left: Add File Button and Add Trigger Button */} -
- - - - - - { - onAddFile?.(); - }} - > - - {t('chat.input-attach-add-files-or-photos')} - - - - + {/* Left: add files/photos + connector picker + skill picker */} +
+ + {onToggleConnectorPanel && ( + + )} + {onToggleSkillPanel && ( + + )}
- {/* Right: Session mode (workspace: full toggle; session: current mode only) + send */} -
- {sessionMode !== undefined && ( - {})} - readOnly={!sessionModeSelectInteractive} - className="shrink-0" - /> - )} + {/* Right: send */} +
{ activeSubTriggerRef.current = e.currentTarget; }} @@ -465,7 +464,7 @@ export function ChatInputModelDropdown({ className="mt-0.5 h-4 w-4 shrink-0" aria-hidden /> - + {t('setting.eigent-cloud')} @@ -493,16 +492,16 @@ export function ChatInputModelDropdown({ { activeSubTriggerRef.current = e.currentTarget; }} > - + {t('setting.custom-model')} @@ -545,7 +544,7 @@ export function ChatInputModelDropdown({ }} className="flex items-center justify-between" > -
+
{modelImage ? (
-
+
{!isConfigured && ( -
+
)} {isPreferred && ( )} {isConfigured && !isPreferred && ( -
+
)}
@@ -585,16 +584,16 @@ export function ChatInputModelDropdown({ { activeSubTriggerRef.current = e.currentTarget; }} > - + {t('setting.local-model')} @@ -615,7 +614,7 @@ export function ChatInputModelDropdown({ }} className="flex items-center justify-between" > -
+
{modelImage ? (
-
+
{!isConfigured && ( -
+
)} {isPreferred && ( )} {isConfigured && !isPreferred && ( -
+
)}
diff --git a/src/components/ChatBox/BottomBox/PickerPanel.tsx b/src/components/ChatBox/BottomBox/PickerPanel.tsx new file mode 100644 index 000000000..d9acd6748 --- /dev/null +++ b/src/components/ChatBox/BottomBox/PickerPanel.tsx @@ -0,0 +1,355 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { proxyFetchGet } from '@/api/http'; +import ellipseIcon from '@/assets/mcp/Ellipse-25.svg'; +import { Button } from '@/components/ui/button'; +import { integrationLeadingIconUrl } from '@/lib/connectorIcons'; +import { + RICH_CONNECTOR_STYLE_CLASSES, + RICH_SKILL_STYLE_CLASSES, + connectorNameToToken, + hashSkillLabel, +} from '@/lib/richText'; +import { skillNameToDirName } from '@/lib/skillToolkit'; +import { cn } from '@/lib/utils'; +import { useSkillsStore } from '@/store/skillsStore'; +import { Check, Plus, Wrench } from 'lucide-react'; +import { Fragment, useEffect, useMemo, useState, type ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +/** + * An item shown in a picker panel. `token` is the exact string inserted inline + * into the rich chat input when the item is selected (`#skill` / `@connector`). + */ +export interface PickerItem { + id: string; + name: string; + token: string; +} + +/** A labelled section within a picker (e.g. built-in vs. your own connectors). */ +export interface PickerGroup { + id: string; + /** Section heading; omit for a single ungrouped list (e.g. skills). */ + label?: string; + items: PickerItem[]; +} + +interface PickerPanelProps { + title: string; + groups: PickerGroup[]; + /** Current input text — an item is "added" when its token appears in it. */ + inputValue: string; + onToggleItem: (item: PickerItem) => void; + /** Leading token tag for a row (`#skill` / `@connector`). */ + renderTag: (item: PickerItem) => ReactNode; + /** Leading logo/icon for a row, shown before the item name. Omit for no logo. */ + renderLogo?: (item: PickerItem) => ReactNode; + loading?: boolean; + emptyLabel: string; + emptyActionLabel: string; + onEmptyAction: () => void; +} + +/** + * Floating list panel shown above BoxMain in the BottomBox shell. Selecting an + * item inserts its token inline into the input; selecting an added item removes + * it. Purely presentational — the trigger and open state live in BottomBox. + */ +export function PickerPanel({ + title, + groups, + inputValue, + onToggleItem, + renderTag, + renderLogo, + loading = false, + emptyLabel, + emptyActionLabel, + onEmptyAction, +}: PickerPanelProps) { + const nonEmptyGroups = groups.filter((g) => g.items.length > 0); + const totalItems = nonEmptyGroups.reduce((n, g) => n + g.items.length, 0); + + return ( +
+ {/* Header */} +
+ + {title} + + {totalItems > 0 && ( + + {totalItems} + + )} +
+ + {/* List: max-h-[240px] caps the panel's scrollable area */} +
+ {loading ? ( + <> + {[0, 1, 2].map((i) => ( +
+ ))} + + ) : totalItems === 0 ? ( +
+ + {emptyLabel} + + +
+ ) : ( + nonEmptyGroups.map((group) => ( + + {group.label && ( +
+ {group.label} +
+ )} + {group.items.map((item) => ( + onToggleItem(item)} + /> + ))} +
+ )) + )} +
+
+ ); +} + +interface PickerPanelItemProps { + item: PickerItem; + tag: ReactNode; + logo?: ReactNode; + added: boolean; + onToggle: () => void; +} + +function PickerPanelItem({ + item, + tag, + logo, + added, + onToggle, +}: PickerPanelItemProps) { + return ( + + ); +} + +interface WiredPickerPanelProps { + inputValue: string; + onToggleItem: (item: PickerItem) => void; +} + +/** Built-in integrations excluded from the MCP connector list (mirrors settings). */ +const EXCLUDED_BUILTIN_CONNECTORS = ['Search', 'RAG']; + +/** + * Full connector list matching the Connectors settings page: built-in + * integrations (`/api/v1/config/info`) plus the user's own MCPs + * (`/api/v1/mcp/users`), shown as two labelled sections. + */ +export function ConnectorPickerPanel({ + inputValue, + onToggleItem, +}: WiredPickerPanelProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [builtIn, setBuiltIn] = useState([]); + const [yourMcps, setYourMcps] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + Promise.allSettled([ + proxyFetchGet('/api/v1/config/info'), + proxyFetchGet('/api/v1/mcp/users'), + ]) + .then(([infoRes, usersRes]) => { + if (cancelled) return; + if ( + infoRes.status === 'fulfilled' && + infoRes.value && + typeof infoRes.value === 'object' + ) { + setBuiltIn( + Object.keys(infoRes.value) + .filter((key) => !EXCLUDED_BUILTIN_CONNECTORS.includes(key)) + .map((key) => ({ + id: `builtin-${key}`, + name: key, + token: connectorNameToToken(key), + })) + ); + } + if (usersRes.status === 'fulfilled') { + const list = Array.isArray(usersRes.value) + ? usersRes.value + : (usersRes.value?.items ?? []); + setYourMcps( + list.map((item: { id: number; mcp_name: string }) => ({ + id: `user-${item.id}`, + name: item.mcp_name, + token: connectorNameToToken(item.mcp_name), + })) + ); + } + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + const groups: PickerGroup[] = [ + { + id: 'builtin', + label: t('setting.mcp-sidebar-built-in'), + items: builtIn, + }, + { id: 'yours', label: t('setting.your-own-mcps'), items: yourMcps }, + ]; + + return ( + ( + + {item.token} + + )} + renderLogo={(item) => { + if (!item.id.startsWith('builtin-')) { + return ( + + ); + } + const iconUrl = integrationLeadingIconUrl(item.name); + return iconUrl ? ( + + ) : ( + + ); + }} + loading={loading} + emptyLabel={t('chat.no-connectors-added')} + emptyActionLabel={t('chat.input-attach-manage-connectors')} + onEmptyAction={() => navigate('/history?tab=connectors')} + /> + ); +} + +/** Lists the user's enabled skills from the skills store. */ +export function SkillPickerPanel({ + inputValue, + onToggleItem, +}: WiredPickerPanelProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const skills = useSkillsStore((s) => s.skills); + + const items = useMemo( + () => + skills + .filter((s) => s.enabled) + .map((s) => ({ + id: s.id, + name: s.name, + token: `#${s.skillDirName || skillNameToDirName(s.name)}`, + })), + [skills] + ); + + return ( + { + const clsIdx = + hashSkillLabel(item.token) % RICH_SKILL_STYLE_CLASSES.length; + return ( + + {item.token} + + ); + }} + emptyLabel={t('chat.no-skills-added')} + emptyActionLabel={t('chat.input-attach-manage-skills')} + onEmptyAction={() => navigate('/history?tab=agents')} + /> + ); +} diff --git a/src/components/ChatBox/BottomBox/QueuedBox.tsx b/src/components/ChatBox/BottomBox/QueuedBox.tsx index 05a5c8f80..06ce118e5 100644 --- a/src/components/ChatBox/BottomBox/QueuedBox.tsx +++ b/src/components/ChatBox/BottomBox/QueuedBox.tsx @@ -44,12 +44,12 @@ export function QueuedBox({ return (
{/* Queuing Header Top */} -
+
{/* Lead Button for expand/collapse */} + + + {EFFORT_OPTIONS.map((effort) => ( + onValueChange?.(effort)} + className="flex items-center justify-between" + > + {effortLabel(effort)} + {value === effort && ( + + )} + + ))} + + + ); +} diff --git a/src/components/ChatBox/BottomBox/UsageLimitBanner.tsx b/src/components/ChatBox/BottomBox/UsageLimitBanner.tsx index f0cf6201f..e3c721f91 100644 --- a/src/components/ChatBox/BottomBox/UsageLimitBanner.tsx +++ b/src/components/ChatBox/BottomBox/UsageLimitBanner.tsx @@ -35,7 +35,7 @@ export function UsageLimitBanner({ return (
void; @@ -48,18 +62,19 @@ interface BottomBoxProps { onSavePlan?: () => void; onEdit?: () => void; - // Task info - taskTime?: string; - taskStatus?: ChatTaskStatusType; - - // Pause/Resume - onPauseResume?: () => void; - pauseResumeLoading?: boolean; - // Input props inputProps: Omit & { className?: string }; usageLimitBanner?: UsageLimitBannerProps | null; + // BoxFooter (project-setup controls: mode + model); omit sessionMode to hide the row. + sessionMode?: SessionModeType; + onSessionModeChange?: (mode: SessionModeType) => void; + /** Interactive during project setup (workspace); once the project starts the row is read-only. */ + sessionModeSelectInteractive?: boolean; + /** Reasoning effort control on the BoxFooter row; omit to hide it. */ + thinkingEffort?: ThinkingEffortType; + onThinkingEffortChange?: (effort: ThinkingEffortType) => void; + // Loading states loading?: boolean; @@ -70,6 +85,7 @@ interface BottomBoxProps { export default function BottomBox({ state, + variant = 'input', queuedMessages = [], onRemoveQueuedMessage, subtitle, @@ -79,6 +95,11 @@ export default function BottomBox({ onEdit, inputProps, usageLimitBanner, + sessionMode, + onSessionModeChange, + sessionModeSelectInteractive = false, + thinkingEffort, + onThinkingEffortChange, loading = false, noModelOverlay = false, onSelectModel, @@ -86,27 +107,116 @@ export default function BottomBox({ const { t } = useTranslation(); const enableQueuedBox = true; //TODO: Fix the reason of queued box disable in https://github.com/eigent-ai/eigent/issues/684 + // Picker panels (connector/skill) float above BoxMain and are mutually exclusive. + const [openPanel, setOpenPanel] = useState(null); + const panelRef = useRef(null); + + const togglePanel = (panel: PickerPanelKind) => + setOpenPanel((prev) => (prev === panel ? null : panel)); + + // Close the floating panel on outside click (ignore the trigger buttons, + // which own their own toggle). + useEffect(() => { + if (!openPanel) return; + const onPointerDown = (e: PointerEvent) => { + const target = e.target as HTMLElement | null; + if ( + panelRef.current?.contains(target ?? null) || + target?.closest('[data-picker-trigger]') + ) { + return; + } + setOpenPanel(null); + }; + document.addEventListener('pointerdown', onPointerDown); + return () => document.removeEventListener('pointerdown', onPointerDown); + }, [openPanel]); + + const inputValue = inputProps.value ?? ''; + + /** Focus the input and drop the caret at the end after a programmatic edit. */ + const focusInputEnd = () => { + requestAnimationFrame(() => { + const el = inputProps.textareaRef?.current; + if (!el) return; + el.focus(); + const range = document.createRange(); + range.selectNodeContents(el); + range.collapse(false); + const sel = window.getSelection(); + sel?.removeAllRanges(); + sel?.addRange(range); + }); + }; + + /** Append a `#skill` / `@connector` token to the input, space-separated. */ + const insertToken = (token: string) => { + const trimmed = inputValue.replace(/\s+$/, ''); + const next = (trimmed.length ? `${trimmed} ` : '') + `${token} `; + inputProps.onChange?.(next); + focusInputEnd(); + }; + + /** Remove a previously inserted token (and one adjacent space) from the input. */ + const removeToken = (token: string) => { + const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const next = inputValue + .replace(new RegExp(`\\s?${escaped}`), '') + .replace(/\s{2,}/g, ' ') + .replace(/^\s+/, ''); + inputProps.onChange?.(next); + focusInputEnd(); + }; + + const toggleToken = (token: string) => + inputValue.includes(token) ? removeToken(token) : insertToken(token); + // Background color reflects current state only - let backgroundClass = 'bg-ds-bg-neutral-subtle-default'; + let backgroundClass = 'bg-ds-bg-neutral-default-default'; if (state === 'confirm' || state === 'save') - backgroundClass = 'bg-ds-bg-completed-subtle-default'; + backgroundClass = 'bg-ds-bg-completed-default-default'; + + const showQueuedBox = enableQueuedBox && queuedMessages.length > 0; + const hasOverlay = showQueuedBox || !!usageLimitBanner || !!openPanel; return ( -
- {/* QueuedBox overlay (should not affect BoxMain layout) */} - {enableQueuedBox && queuedMessages.length > 0 && ( -
- +
+ {/* Floating overlays: never affect BoxMain layout */} + {hasOverlay && ( +
+ {showQueuedBox && ( + + )} + {usageLimitBanner && } + {openPanel && ( +
+ {openPanel === 'connector' ? ( + toggleToken(item.token)} + /> + ) : ( + toggleToken(item.token)} + /> + )} +
+ )} + {/* future: human-in-the-loop approval cards mount here */}
)} {/* BoxMain */}
- {/* BoxHeader variants */} + {/* BoxHeader variants — project confirmation */} {state === 'confirm' && ( )} - {/* Inputbox (always visible) */} - {usageLimitBanner && } - + {/* Main box — variant slot */} + {variant === 'input' && ( + togglePanel('connector')} + skillPanelOpen={openPanel === 'skill'} + onToggleSkillPanel={() => togglePanel('skill')} + /> + )} + + {/* Box footer — project-setup controls (mode + model); read-only once started */} + {sessionMode !== undefined && ( + + )} {noModelOverlay && onSelectModel ? (
e.stopPropagation()} > {seg.text} @@ -82,12 +83,25 @@ function renderMessageRichSegments(text: string, keyPrefix: string): ReactNode { } return {seg.text}; } + if (seg.type === 'connector') { + return ( + + {seg.text} + + ); + } const clsIdx = hashSkillLabel(seg.text) % RICH_SKILL_STYLE_CLASSES.length; return ( @@ -151,7 +165,7 @@ export function UserMessageRichContent({ }} title="Open skill folder" className={cn( - 'mx-0 rounded px-0.5 font-normal inline cursor-pointer align-baseline [font:inherit] hover:opacity-90', + 'mx-0 inline cursor-pointer rounded-lg px-1 align-baseline font-normal [font:inherit] hover:opacity-90', RICH_SKILL_STYLE_CLASSES[clsIdx] )} > diff --git a/src/components/ChatBox/README.md b/src/components/ChatBox/README.md index f123c4d38..0487927a3 100644 --- a/src/components/ChatBox/README.md +++ b/src/components/ChatBox/README.md @@ -41,8 +41,7 @@ ChatBox/ ├── index.tsx ├── InputBox.tsx ├── RichChatInput.tsx - ├── ChatInputModelDropdown.tsx - ├── BoxAction.tsx + ├── ModelSelect.tsx ├── BoxHeader.tsx └── QueuedBox.tsx ``` @@ -91,9 +90,8 @@ ChatBox/ ## BottomBox (`BottomBox/`) -- **`index.tsx`**: Wires `BoxHeader`, `InputBox` / `RichChatInput`, `BoxAction`, `QueuedBox` to task state (pending, running, confirm, etc.). -- **`InputBox` / `RichChatInput` / `ChatInputModelDropdown`**: Text input, model picker, rich input where applicable. -- **`BoxAction`**: Confirm, edit, send, stop, and related actions. +- **`index.tsx`**: Wires `BoxHeader`, `InputBox` / `RichChatInput`, `QueuedBox` to task state (pending, running, confirm, etc.). +- **`InputBox` / `RichChatInput` / `ModelSelect`**: Text input, model picker, rich input where applicable. - **`BoxHeader`**: Task summary, timing, and header affordances. - **`QueuedBox`**: Queued user messages when the task pipeline is busy. diff --git a/src/components/ChatBox/TaskBox/PlanTaskBox/ExpandedOverlay.tsx b/src/components/ChatBox/TaskBox/PlanTaskBox/ExpandedOverlay.tsx index 5ff874af5..a73f6494c 100644 --- a/src/components/ChatBox/TaskBox/PlanTaskBox/ExpandedOverlay.tsx +++ b/src/components/ChatBox/TaskBox/PlanTaskBox/ExpandedOverlay.tsx @@ -86,7 +86,7 @@ export function ExpandedOverlay({ }} >
-
+
{t('chat.subtasks-planning')} diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index b6b844ebc..adc6d524e 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -36,7 +36,12 @@ import { buildProjectContinuationContext } from '@/store/chatStore'; import { usePageTabStore } from '@/store/pageTabStore'; import { useSpaceStore } from '@/store/spaceStore'; import { ExecutionStatus } from '@/types'; -import { AgentStep, ChatTaskStatus, SessionMode } from '@/types/constants'; +import { + AgentStep, + ChatTaskStatus, + SessionMode, + ThinkingEffort, +} from '@/types/constants'; import { useCallback, useEffect, @@ -227,6 +232,8 @@ export default function ChatBox(): JSX.Element { activeProjectMode ?? inferredSessionMode ?? SessionMode.SINGLE_AGENT; const displaySessionMode = activeProjectMode ?? inferredSessionMode ?? undefined; + // Not yet persisted per-project (unlike session mode) — fixed display value for now. + const displayThinkingEffort = ThinkingEffort.MEDIUM; const ensureActiveProjectMode = useCallback(() => { const projectId = projectStore.activeProjectId; if (!projectId || activeProjectMode) return; @@ -1248,10 +1255,10 @@ export default function ChatBox(): JSX.Element { const chatColumn = ( <> {/* Main: scroll (scrollbar on panel edge) + BottomBox overlay when chatting */} -
+
{hasAnyMessages ? ( ) : (
-
+
{chatStore.activeTaskId && ( )}
@@ -1309,9 +1317,9 @@ export default function ChatBox(): JSX.Element {
-
+
@@ -1388,7 +1393,7 @@ export default function ChatBox(): JSX.Element { ); return ( -
+
{chatColumn}
); diff --git a/src/components/Workspace/ProjectModeToggle.tsx b/src/components/Workspace/ProjectModeToggle.tsx index c171a3395..66dd6f9f3 100644 --- a/src/components/Workspace/ProjectModeToggle.tsx +++ b/src/components/Workspace/ProjectModeToggle.tsx @@ -63,6 +63,8 @@ export interface ProjectModeToggleProps { * bar (see model read-only chip). */ readOnly?: boolean; + /** When true, hides the text label and shows only the leading icon (narrow footer). */ + compact?: boolean; } export function ProjectModeToggle({ @@ -70,6 +72,7 @@ export function ProjectModeToggle({ onValueChange, className, readOnly = false, + compact = false, }: ProjectModeToggleProps) { const { t } = useTranslation(); const chevronScale = useAnimationControls(); @@ -82,6 +85,7 @@ export function ProjectModeToggle({ const isSingle = value === SessionMode.SINGLE_AGENT; const nextMode = isSingle ? SessionMode.WORKFORCE : SessionMode.SINGLE_AGENT; const label = isSingle ? labelSingle : labelWorkforce; + const LeadingIcon = isSingle ? Joystick : Gamepad2; const toggle = () => onValueChange(nextMode); @@ -92,8 +96,6 @@ export function ProjectModeToggle({ })(); }, [chevronScale]); - const LeadingIcon = isSingle ? Joystick : Gamepad2; - const shellClass = cn( 'rounded-xl px-2 py-1 inline-flex items-center gap-1.5', 'bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default', @@ -116,7 +118,8 @@ export function ProjectModeToggle({ return (
@@ -125,7 +128,9 @@ export function ProjectModeToggle({ strokeWidth={2} aria-hidden /> - {label} + {!compact && ( + {label} + )}
); @@ -137,10 +142,11 @@ export function ProjectModeToggle({ - - {label} - + {!compact && ( + + {label} + + )} diff --git a/src/components/Workspace/index.tsx b/src/components/Workspace/index.tsx index 39972ff35..303c5c3de 100644 --- a/src/components/Workspace/index.tsx +++ b/src/components/Workspace/index.tsx @@ -44,6 +44,8 @@ import { ChatTaskStatus, SessionMode, type SessionModeType, + ThinkingEffort, + type ThinkingEffortType, } from '@/types/constants'; import { ArrowLeft } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -199,6 +201,9 @@ export default function Workspace({ SessionMode.SINGLE_AGENT ); const effectiveSessionMode = controlledSessionMode ?? draftSessionMode; + // Not yet persisted per-project (unlike session mode) — local to the composer for now. + const [draftThinkingEffort, setDraftThinkingEffort] = + useState(ThinkingEffort.MEDIUM); const setActiveProjectMode = useCallback( (mode: SessionModeType) => { @@ -390,9 +395,6 @@ export default function Workspace({ 'Legacy Spaces are read-only. Create a new Space to start a Project.', }) : t('layout.project-task-placeholder'), - sessionMode: effectiveSessionMode, - onSessionModeChange: setActiveProjectMode, - sessionModeSelectInteractive: true, }); const taskAssigning = @@ -515,6 +517,11 @@ export default function Workspace({ noModelOverlay={!hasModel} onSelectModel={() => navigate('/history?tab=agents')} inputProps={buildComposerInputProps()} + sessionMode={effectiveSessionMode} + onSessionModeChange={setActiveProjectMode} + sessionModeSelectInteractive + thinkingEffort={draftThinkingEffort} + onThinkingEffortChange={setDraftThinkingEffort} />
(threshold: number) { + const ref = useRef(null); + const [compact, setCompact] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el || typeof ResizeObserver === 'undefined') return; + + const update = (width: number) => setCompact(width < threshold); + update(el.getBoundingClientRect().width); + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) update(entry.contentRect.width); + }); + observer.observe(el); + return () => observer.disconnect(); + }, [threshold]); + + return [ref, compact] as const; +} diff --git a/src/i18n/locales/ar/chat.json b/src/i18n/locales/ar/chat.json index f724b85bf..7eb3fcc10 100644 --- a/src/i18n/locales/ar/chat.json +++ b/src/i18n/locales/ar/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "فتح متصفح", "input-attach-manage-browsers": "إدارة المتصفحات", "input-attach-menu-trigger": "إضافة ملفات أو صور، أو فتح المهارات أو الموصلات أو المتصفح من القائمة", + "input-attach-connectors": "الموصلات", + "input-add-connector": "إضافة موصلات", + "input-add-skill": "إضافة مهارات", + "no-connectors-added": "لم تتم إضافة أي موصلات بعد", + "no-skills-added": "لم تتم إضافة أي مهارات بعد", "subtasks-planning": "تخطيط المهام الفرعية", "expand-plan": "توسيع الخطة", "minimize-plan": "تصغير الخطة", diff --git a/src/i18n/locales/ar/layout.json b/src/i18n/locales/ar/layout.json index 9a5980c87..c6b4c012b 100644 --- a/src/i18n/locales/ar/layout.json +++ b/src/i18n/locales/ar/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "خطوط", "onboarding-setup-bg-dotted": "منقط", "onboarding-setup-bg-dashed": "متقطع", - "onboarding-setup-get-started": "ابدأ الآن" + "onboarding-setup-get-started": "ابدأ الآن", + "thinking-effort-label": "مستوى التفكير", + "thinking-effort-light": "خفيف", + "thinking-effort-medium": "متوسط", + "thinking-effort-high": "مرتفع", + "thinking-effort-extra_high": "مرتفع جدًا", + "thinking-effort-ultra": "فائق" } diff --git a/src/i18n/locales/de/chat.json b/src/i18n/locales/de/chat.json index 41f6f73b5..47f0683ed 100644 --- a/src/i18n/locales/de/chat.json +++ b/src/i18n/locales/de/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "Browser öffnen", "input-attach-manage-browsers": "Browser verwalten", "input-attach-menu-trigger": "Dateien oder Fotos hinzufügen oder Skills, Connectors oder Browser über das Menü öffnen", + "input-attach-connectors": "Connectors", + "input-add-connector": "Connectors hinzufügen", + "input-add-skill": "Skills hinzufügen", + "no-connectors-added": "Noch keine Connectors hinzugefügt", + "no-skills-added": "Noch keine Skills hinzugefügt", "subtasks-planning": "Teilaufgabenplanung", "expand-plan": "Plan erweitern", "minimize-plan": "Plan minimieren", diff --git a/src/i18n/locales/de/layout.json b/src/i18n/locales/de/layout.json index d6ee251e3..40ca1dc99 100644 --- a/src/i18n/locales/de/layout.json +++ b/src/i18n/locales/de/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "Liniert", "onboarding-setup-bg-dotted": "Gepunktet", "onboarding-setup-bg-dashed": "Gestrichelt", - "onboarding-setup-get-started": "Loslegen" + "onboarding-setup-get-started": "Loslegen", + "thinking-effort-label": "Denkaufwand", + "thinking-effort-light": "Niedrig", + "thinking-effort-medium": "Mittel", + "thinking-effort-high": "Hoch", + "thinking-effort-extra_high": "Sehr hoch", + "thinking-effort-ultra": "Ultra" } diff --git a/src/i18n/locales/en-us/chat.json b/src/i18n/locales/en-us/chat.json index e7b106182..05ad3aa1a 100644 --- a/src/i18n/locales/en-us/chat.json +++ b/src/i18n/locales/en-us/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "Open a browser", "input-attach-manage-browsers": "Manage browsers", "input-attach-menu-trigger": "Add files or photos, or open Skills, Connectors, or Browser from the menu", + "input-attach-connectors": "Connectors", + "input-add-connector": "Add connectors", + "input-add-skill": "Add skills", + "no-connectors-added": "No connectors added yet", + "no-skills-added": "No skills added yet", "subtasks-planning": "Subtasks Planning", "expand-plan": "Expand plan", "minimize-plan": "Minimize plan", diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index 14ca3d7ed..dfacb4fd2 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "Ruled", "onboarding-setup-bg-dotted": "Dotted", "onboarding-setup-bg-dashed": "Dashed", - "onboarding-setup-get-started": "Get Started" + "onboarding-setup-get-started": "Get Started", + "thinking-effort-label": "Thinking effort", + "thinking-effort-light": "Light", + "thinking-effort-medium": "Medium", + "thinking-effort-high": "High", + "thinking-effort-extra_high": "Extra High", + "thinking-effort-ultra": "Ultra" } diff --git a/src/i18n/locales/es/chat.json b/src/i18n/locales/es/chat.json index 1ff4b92ba..9aa1e1a85 100644 --- a/src/i18n/locales/es/chat.json +++ b/src/i18n/locales/es/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "Abrir un navegador", "input-attach-manage-browsers": "Gestionar navegadores", "input-attach-menu-trigger": "Añadir archivos o fotos o abrir Habilidades, Conectores o Navegador desde el menú", + "input-attach-connectors": "Conectores", + "input-add-connector": "Añadir conectores", + "input-add-skill": "Añadir habilidades", + "no-connectors-added": "Aún no se han añadido conectores", + "no-skills-added": "Aún no se han añadido habilidades", "subtasks-planning": "Planificación de subtareas", "expand-plan": "Expandir plan", "minimize-plan": "Minimizar plan", diff --git a/src/i18n/locales/es/layout.json b/src/i18n/locales/es/layout.json index 9f73c7a88..e7a726ae8 100644 --- a/src/i18n/locales/es/layout.json +++ b/src/i18n/locales/es/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "Rayado", "onboarding-setup-bg-dotted": "Punteado", "onboarding-setup-bg-dashed": "Discontinuo", - "onboarding-setup-get-started": "Empezar" + "onboarding-setup-get-started": "Empezar", + "thinking-effort-label": "Esfuerzo de razonamiento", + "thinking-effort-light": "Bajo", + "thinking-effort-medium": "Medio", + "thinking-effort-high": "Alto", + "thinking-effort-extra_high": "Muy alto", + "thinking-effort-ultra": "Ultra" } diff --git a/src/i18n/locales/fr/chat.json b/src/i18n/locales/fr/chat.json index 6e6be3659..c77c408aa 100644 --- a/src/i18n/locales/fr/chat.json +++ b/src/i18n/locales/fr/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "Ouvrir un navigateur", "input-attach-manage-browsers": "Gérer les navigateurs", "input-attach-menu-trigger": "Ajouter fichiers ou photos ou ouvrir Compétences, Connecteurs ou Navigateur depuis le menu", + "input-attach-connectors": "Connecteurs", + "input-add-connector": "Ajouter des connecteurs", + "input-add-skill": "Ajouter des compétences", + "no-connectors-added": "Aucun connecteur ajouté pour le moment", + "no-skills-added": "Aucune compétence ajoutée pour le moment", "subtasks-planning": "Planification des sous-tâches", "expand-plan": "Développer le plan", "minimize-plan": "Réduire le plan", diff --git a/src/i18n/locales/fr/layout.json b/src/i18n/locales/fr/layout.json index ffda5ec22..6806e9523 100644 --- a/src/i18n/locales/fr/layout.json +++ b/src/i18n/locales/fr/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "Ligné", "onboarding-setup-bg-dotted": "Pointillé", "onboarding-setup-bg-dashed": "Tirets", - "onboarding-setup-get-started": "Commencer" + "onboarding-setup-get-started": "Commencer", + "thinking-effort-label": "Effort de réflexion", + "thinking-effort-light": "Léger", + "thinking-effort-medium": "Moyen", + "thinking-effort-high": "Élevé", + "thinking-effort-extra_high": "Très élevé", + "thinking-effort-ultra": "Ultra" } diff --git a/src/i18n/locales/it/chat.json b/src/i18n/locales/it/chat.json index 8257018cd..a8a3e5bdb 100644 --- a/src/i18n/locales/it/chat.json +++ b/src/i18n/locales/it/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "Apri un browser", "input-attach-manage-browsers": "Gestisci i browser", "input-attach-menu-trigger": "Aggiungi file o foto o apri Skill, Connettori o Browser dal menu", + "input-attach-connectors": "Connettori", + "input-add-connector": "Aggiungi connettori", + "input-add-skill": "Aggiungi skill", + "no-connectors-added": "Nessun connettore aggiunto", + "no-skills-added": "Nessuna skill aggiunta", "subtasks-planning": "Pianificazione delle sottoattività", "expand-plan": "Espandi piano", "minimize-plan": "Riduci piano", diff --git a/src/i18n/locales/it/layout.json b/src/i18n/locales/it/layout.json index 34e24d9ee..7f842a450 100644 --- a/src/i18n/locales/it/layout.json +++ b/src/i18n/locales/it/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "Righe", "onboarding-setup-bg-dotted": "Punteggiato", "onboarding-setup-bg-dashed": "Tratteggiato", - "onboarding-setup-get-started": "Inizia" + "onboarding-setup-get-started": "Inizia", + "thinking-effort-label": "Impegno di ragionamento", + "thinking-effort-light": "Basso", + "thinking-effort-medium": "Medio", + "thinking-effort-high": "Alto", + "thinking-effort-extra_high": "Molto alto", + "thinking-effort-ultra": "Ultra" } diff --git a/src/i18n/locales/ja/chat.json b/src/i18n/locales/ja/chat.json index 535072968..6701e14cc 100644 --- a/src/i18n/locales/ja/chat.json +++ b/src/i18n/locales/ja/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "ブラウザを開く", "input-attach-manage-browsers": "ブラウザを管理", "input-attach-menu-trigger": "ファイル・写真の追加、またはメニューからスキル・コネクタ・ブラウザを開く", + "input-attach-connectors": "コネクタ", + "input-add-connector": "コネクタを追加", + "input-add-skill": "スキルを追加", + "no-connectors-added": "コネクタはまだ追加されていません", + "no-skills-added": "スキルはまだ追加されていません", "subtasks-planning": "サブタスクの計画", "expand-plan": "計画を展開", "minimize-plan": "計画を最小化", diff --git a/src/i18n/locales/ja/layout.json b/src/i18n/locales/ja/layout.json index abf0a9bfe..6106dd0da 100644 --- a/src/i18n/locales/ja/layout.json +++ b/src/i18n/locales/ja/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "罫線", "onboarding-setup-bg-dotted": "点線", "onboarding-setup-bg-dashed": "破線", - "onboarding-setup-get-started": "はじめる" + "onboarding-setup-get-started": "はじめる", + "thinking-effort-label": "思考強度", + "thinking-effort-light": "軽度", + "thinking-effort-medium": "中程度", + "thinking-effort-high": "高度", + "thinking-effort-extra_high": "非常に高い", + "thinking-effort-ultra": "ウルトラ" } diff --git a/src/i18n/locales/ko/chat.json b/src/i18n/locales/ko/chat.json index a5f785eb0..5852654cc 100644 --- a/src/i18n/locales/ko/chat.json +++ b/src/i18n/locales/ko/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "브라우저 열기", "input-attach-manage-browsers": "브라우저 관리", "input-attach-menu-trigger": "파일·사진 추가 또는 메뉴에서 스킬, 커넥터, 브라우저 열기", + "input-attach-connectors": "커넥터", + "input-add-connector": "커넥터 추가", + "input-add-skill": "스킬 추가", + "no-connectors-added": "아직 추가된 커넥터가 없습니다", + "no-skills-added": "아직 추가된 스킬이 없습니다", "subtasks-planning": "하위 작업 계획", "expand-plan": "계획 펼치기", "minimize-plan": "계획 최소화", diff --git a/src/i18n/locales/ko/layout.json b/src/i18n/locales/ko/layout.json index 0fff60c7c..34201deff 100644 --- a/src/i18n/locales/ko/layout.json +++ b/src/i18n/locales/ko/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "줄", "onboarding-setup-bg-dotted": "점선", "onboarding-setup-bg-dashed": "파선", - "onboarding-setup-get-started": "시작하기" + "onboarding-setup-get-started": "시작하기", + "thinking-effort-label": "사고 강도", + "thinking-effort-light": "낮음", + "thinking-effort-medium": "보통", + "thinking-effort-high": "높음", + "thinking-effort-extra_high": "매우 높음", + "thinking-effort-ultra": "울트라" } diff --git a/src/i18n/locales/ru/chat.json b/src/i18n/locales/ru/chat.json index 777862b5d..7904c2c23 100644 --- a/src/i18n/locales/ru/chat.json +++ b/src/i18n/locales/ru/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "Открыть браузер", "input-attach-manage-browsers": "Управлять браузерами", "input-attach-menu-trigger": "Добавить файлы или фото или открыть Навыки, Коннекторы или Браузер из меню", + "input-attach-connectors": "Коннекторы", + "input-add-connector": "Добавить коннекторы", + "input-add-skill": "Добавить навыки", + "no-connectors-added": "Коннекторы пока не добавлены", + "no-skills-added": "Навыки пока не добавлены", "subtasks-planning": "Планирование подзадач", "expand-plan": "Развернуть план", "minimize-plan": "Свернуть план", diff --git a/src/i18n/locales/ru/layout.json b/src/i18n/locales/ru/layout.json index d38f14828..3a7e30377 100644 --- a/src/i18n/locales/ru/layout.json +++ b/src/i18n/locales/ru/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "Линии", "onboarding-setup-bg-dotted": "Пунктир", "onboarding-setup-bg-dashed": "Штрихи", - "onboarding-setup-get-started": "Начать" + "onboarding-setup-get-started": "Начать", + "thinking-effort-label": "Глубина размышлений", + "thinking-effort-light": "Лёгкая", + "thinking-effort-medium": "Средняя", + "thinking-effort-high": "Высокая", + "thinking-effort-extra_high": "Очень высокая", + "thinking-effort-ultra": "Ультра" } diff --git a/src/i18n/locales/zh-Hans/chat.json b/src/i18n/locales/zh-Hans/chat.json index db606ec50..86725d070 100644 --- a/src/i18n/locales/zh-Hans/chat.json +++ b/src/i18n/locales/zh-Hans/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "打开浏览器", "input-attach-manage-browsers": "管理浏览器", "input-attach-menu-trigger": "添加文件、照片,或通过菜单打开技能、连接器、浏览器", + "input-attach-connectors": "连接器", + "input-add-connector": "添加连接器", + "input-add-skill": "添加技能", + "no-connectors-added": "尚未添加连接器", + "no-skills-added": "尚未添加技能", "subtasks-planning": "子任务规划", "expand-plan": "展开计划", "minimize-plan": "最小化计划", diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index b79ea9d72..f3d9d13d1 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "横线", "onboarding-setup-bg-dotted": "点线", "onboarding-setup-bg-dashed": "虚线", - "onboarding-setup-get-started": "开始使用" + "onboarding-setup-get-started": "开始使用", + "thinking-effort-label": "思考强度", + "thinking-effort-light": "轻度", + "thinking-effort-medium": "中度", + "thinking-effort-high": "高度", + "thinking-effort-extra_high": "极高", + "thinking-effort-ultra": "超高" } diff --git a/src/i18n/locales/zh-Hant/chat.json b/src/i18n/locales/zh-Hant/chat.json index 58c59c761..976fec5ed 100644 --- a/src/i18n/locales/zh-Hant/chat.json +++ b/src/i18n/locales/zh-Hant/chat.json @@ -103,6 +103,11 @@ "input-attach-open-browser": "開啟瀏覽器", "input-attach-manage-browsers": "管理瀏覽器", "input-attach-menu-trigger": "新增檔案、相片,或從選單開啟技能、連接器、瀏覽器", + "input-attach-connectors": "連接器", + "input-add-connector": "新增連接器", + "input-add-skill": "新增技能", + "no-connectors-added": "尚未新增連接器", + "no-skills-added": "尚未新增技能", "subtasks-planning": "子任務規劃", "expand-plan": "展開計劃", "minimize-plan": "最小化計劃", diff --git a/src/i18n/locales/zh-Hant/layout.json b/src/i18n/locales/zh-Hant/layout.json index 4f037661c..f6340afa9 100644 --- a/src/i18n/locales/zh-Hant/layout.json +++ b/src/i18n/locales/zh-Hant/layout.json @@ -514,5 +514,11 @@ "onboarding-setup-bg-ruled": "橫線", "onboarding-setup-bg-dotted": "點線", "onboarding-setup-bg-dashed": "虛線", - "onboarding-setup-get-started": "開始使用" + "onboarding-setup-get-started": "開始使用", + "thinking-effort-label": "思考強度", + "thinking-effort-light": "輕度", + "thinking-effort-medium": "中度", + "thinking-effort-high": "高度", + "thinking-effort-extra_high": "極高", + "thinking-effort-ultra": "超高" } diff --git a/src/lib/connectorIcons.ts b/src/lib/connectorIcons.ts new file mode 100644 index 000000000..cf17041a3 --- /dev/null +++ b/src/lib/connectorIcons.ts @@ -0,0 +1,62 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import cursorIcon from '@/assets/icon/cursor.svg'; +import githubIcon from '@/assets/icon/github.svg'; +import googleCalendarIcon from '@/assets/icon/google_calendar.svg'; +import googleGmailIcon from '@/assets/icon/google_gmail.svg'; +import larkIcon from '@/assets/icon/lark.png'; +import linkedinIcon from '@/assets/icon/linkedin.svg'; +import notionIcon from '@/assets/icon/notion.svg'; +import ragIcon from '@/assets/icon/rag.svg'; +import redditIcon from '@/assets/icon/reddit.svg'; +import slackIcon from '@/assets/icon/slack.svg'; +import telegramIcon from '@/assets/icon/telegram.svg'; +import vsCodeIcon from '@/assets/icon/vs-code.svg'; +import whatsappIcon from '@/assets/icon/whatsapp.svg'; +import xIcon from '@/assets/icon/x.svg'; + +/** + * Brand logo per built-in integration key, keyed by the lowercased, + * trimmed name returned from `/api/v1/config/info` (matches the Connectors + * settings page). Shared so any connector-picking UI (Settings, chat input + * picker, etc.) shows the same logos. + */ +export const INTEGRATION_ICON_BY_KEY: Record = { + notion: notionIcon, + slack: slackIcon, + 'google calendar': googleCalendarIcon, + gmail: googleGmailIcon, + 'google gmail': googleGmailIcon, + linkedin: linkedinIcon, + lark: larkIcon, + rag: ragIcon, + telegram: telegramIcon, + whatsapp: whatsappIcon, + x: xIcon, + 'x(twitter)': xIcon, + twitter: xIcon, + reddit: redditIcon, + github: githubIcon, + cursor: cursorIcon, + 'vs code': vsCodeIcon, + vscode: vsCodeIcon, +}; + +/** Looks up a built-in integration's brand logo by name (case/whitespace-insensitive). */ +export function integrationLeadingIconUrl( + integrationKey: string +): string | undefined { + return INTEGRATION_ICON_BY_KEY[integrationKey.toLowerCase().trim()]; +} diff --git a/src/lib/richText.ts b/src/lib/richText.ts index 68b82ffce..e9a05c94c 100644 --- a/src/lib/richText.ts +++ b/src/lib/richText.ts @@ -12,15 +12,31 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -/** Shared by `RichChatInput` and `UserMessageRichContent` (URLs, #skills). */ -export type RichSegment = { type: 'text' | 'url' | 'skill'; text: string }; +/** Shared by `RichChatInput` and `UserMessageRichContent` (URLs, #skills, @connectors). */ +export type RichSegment = { + type: 'text' | 'url' | 'skill' | 'connector'; + text: string; +}; + +/** Chip styling shared by the input HTML and the message-body renderer. */ +export const RICH_CONNECTOR_STYLE_CLASSES = + 'text-ds-text-information-default-default bg-ds-bg-neutral-default-default'; + +/** `@token` inserted into the input for a connector; spaces collapse to `_`. */ +export function connectorNameToToken(name: string): string { + const slug = name + .trim() + .replace(/\s+/g, '_') + .replace(/[^A-Za-z0-9_-]/g, ''); + return `@${slug || 'connector'}`; +} /** Hash-stable palette: semantic “other” tones (not default body text). Shared by input + message body. */ export const RICH_SKILL_STYLE_CLASSES = [ - 'text-ds-text-success-default-default bg-ds-bg-neutral-subtle-disabled', - 'text-ds-text-warning-default-default bg-ds-bg-neutral-subtle-disabled', - 'text-ds-text-terminal-default-default bg-ds-bg-neutral-subtle-disabled', - 'text-ds-text-document-default-default bg-ds-bg-neutral-subtle-disabled', + 'text-ds-text-success-default-default bg-ds-bg-neutral-default-default', + 'text-ds-text-warning-default-default bg-ds-bg-neutral-default-default', + 'text-ds-text-terminal-default-default bg-ds-bg-neutral-default-default', + 'text-ds-text-document-default-default bg-ds-bg-neutral-default-default', ] as const; export function hashSkillLabel(label: string): number { @@ -38,8 +54,17 @@ export function trimUrlTail(raw: string): string { } const URL_AT_START = /^(https?:\/\/[^\s<>"']+|www\.[^\s<>"']+)/i; +const SKILL_AT_START = /^#([a-zA-Z0-9_-]+)/; +const CONNECTOR_AT_START = /^@([A-Za-z0-9_-]+)/; -/** Plain-text tokenizer: http(s) / www URLs and #skill tokens (alphanumeric + _-). */ +/** True when `@` here begins a connector token rather than an email tail (`me@host`). */ +function isConnectorStart(text: string, at: number): boolean { + const prev = text[at - 1]; + if (prev && /[A-Za-z0-9_]/.test(prev)) return false; + return CONNECTOR_AT_START.test(text.slice(at)); +} + +/** Plain-text tokenizer: URLs, #skill tokens, and @connector tokens. */ export function tokenizeRichPlainText(text: string): RichSegment[] { const out: RichSegment[] = []; let i = 0; @@ -62,7 +87,7 @@ export function tokenizeRichPlainText(text: string): RichSegment[] { } if (slice[0] === '#') { - const skillMatch = slice.match(/^#([a-zA-Z0-9_-]+)/); + const skillMatch = slice.match(SKILL_AT_START); if (skillMatch) { out.push({ type: 'skill', text: skillMatch[0] }); i += skillMatch[0].length; @@ -70,11 +95,21 @@ export function tokenizeRichPlainText(text: string): RichSegment[] { } } + if (slice[0] === '@' && isConnectorStart(text, i)) { + const connMatch = slice.match(CONNECTOR_AT_START); + if (connMatch) { + out.push({ type: 'connector', text: connMatch[0] }); + i += connMatch[0].length; + continue; + } + } + let j = i + 1; while (j < len) { const tail = text.slice(j); if (URL_AT_START.test(tail)) break; - if (text[j] === '#' && /^#([a-zA-Z0-9_-]+)/.test(tail)) break; + if (text[j] === '#' && SKILL_AT_START.test(tail)) break; + if (text[j] === '@' && isConnectorStart(text, j)) break; j++; } out.push({ type: 'text', text: text.slice(i, j) }); @@ -127,11 +162,17 @@ export function segmentsToHtml(segments: RichSegment[]): string { } else { parts.push(safe); } + } else if (seg.type === 'connector') { + parts.push( + `${escapeHtml( + seg.text + )}` + ); } else { const idx = hashSkillLabel(seg.text) % RICH_SKILL_STYLE_CLASSES.length; const cls = RICH_SKILL_STYLE_CLASSES[idx]; parts.push( - `${escapeHtml(seg.text)}` + `${escapeHtml(seg.text)}` ); } } diff --git a/src/pages/Connectors/MCP.tsx b/src/pages/Connectors/MCP.tsx index b1aa11e52..dfde0273f 100644 --- a/src/pages/Connectors/MCP.tsx +++ b/src/pages/Connectors/MCP.tsx @@ -21,21 +21,7 @@ import { proxyFetchPost, proxyFetchPut, } from '@/api/http'; -import cursorIcon from '@/assets/icon/cursor.svg'; -import githubIcon from '@/assets/icon/github.svg'; import googleIcon from '@/assets/icon/google.svg'; -import googleCalendarIcon from '@/assets/icon/google_calendar.svg'; -import googleGmailIcon from '@/assets/icon/google_gmail.svg'; -import larkIcon from '@/assets/icon/lark.png'; -import linkedinIcon from '@/assets/icon/linkedin.svg'; -import notionIcon from '@/assets/icon/notion.svg'; -import ragIcon from '@/assets/icon/rag.svg'; -import redditIcon from '@/assets/icon/reddit.svg'; -import slackIcon from '@/assets/icon/slack.svg'; -import telegramIcon from '@/assets/icon/telegram.svg'; -import vsCodeIcon from '@/assets/icon/vs-code.svg'; -import whatsappIcon from '@/assets/icon/whatsapp.svg'; -import xIcon from '@/assets/icon/x.svg'; import ellipseIcon from '@/assets/mcp/Ellipse-25.svg'; import SearchInput from '@/components/Dashboard/SearchInput'; import { Button } from '@/components/ui/button'; @@ -52,6 +38,7 @@ import { type IntegrationItem, } from '@/hooks/useIntegrationManagement'; import { capitalizeFirstLetter, getProxyBaseURL } from '@/lib'; +import { integrationLeadingIconUrl } from '@/lib/connectorIcons'; import { useAuthStore } from '@/store/authStore'; import { motion } from 'framer-motion'; import { @@ -90,31 +77,6 @@ const COMING_SOON_NAMES = [ 'Github', ] as const; -const INTEGRATION_ICON_BY_KEY: Record = { - notion: notionIcon, - slack: slackIcon, - 'google calendar': googleCalendarIcon, - gmail: googleGmailIcon, - 'google gmail': googleGmailIcon, - linkedin: linkedinIcon, - lark: larkIcon, - rag: ragIcon, - telegram: telegramIcon, - whatsapp: whatsappIcon, - x: xIcon, - 'x(twitter)': xIcon, - twitter: xIcon, - reddit: redditIcon, - github: githubIcon, - cursor: cursorIcon, - 'vs code': vsCodeIcon, - vscode: vsCodeIcon, -}; - -function integrationLeadingIconUrl(integrationKey: string): string | undefined { - return INTEGRATION_ICON_BY_KEY[integrationKey.toLowerCase().trim()]; -} - export default function SettingMCP() { const { checkAgentTool } = useAuthStore(); const { t } = useTranslation(); diff --git a/src/types/constants.ts b/src/types/constants.ts index 8cabdadf2..2592aec4d 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -110,3 +110,17 @@ export const SessionMode = { } as const; export type SessionModeType = (typeof SessionMode)[keyof typeof SessionMode]; + +/** + * Reasoning/thinking effort level for the active model, low → high compute budget. + */ +export const ThinkingEffort = { + LIGHT: 'light', + MEDIUM: 'medium', + HIGH: 'high', + EXTRA_HIGH: 'extra_high', + ULTRA: 'ultra', +} as const; + +export type ThinkingEffortType = + (typeof ThinkingEffort)[keyof typeof ThinkingEffort];