mirror of
https://github.com/csunny/DB-GPT.git
synced 2026-07-16 17:15:22 +00:00
fix(web): repair merge conflicts in chat input area + clean up ManusLeftPanel
- index.tsx: 修复合并导致的 JSX 结构错乱(重复的 Voice/Send 按钮、孤儿块、 错位的 )}),恢复单一聊天输入区工具栏,融合 main 的 ContextUsageBar/taskPlan 与本分支的 connector 选择器 - ManusLeftPanel.tsx: 清理未使用变量(hasObservations/hasContent/contentExpanded/ 未用的 t),给保留接口的 onShare/setDownloading 加 _ 前缀,修复 getTodoStep* 的 TFunction 类型断言,prettier 格式化 import - en/chat.ts: use_connector 文案统一为 'Select MCP' - tongyi.toml: 新增 agent_context 上下文窗口预算配置 - scheduled_task/serve.py: 增加 DBGPT_CHAT_TASK_SCHEDULER_ENABLED 开关与内存 jobstore Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,21 @@ encrypt_key = "your_secret_key"
|
||||
host = "0.0.0.0"
|
||||
port = 5670
|
||||
|
||||
[service.web.agent_context]
|
||||
# Agent context-window budget. Set max_context_tokens to 0 to auto-detect from
|
||||
# the selected model's metadata. The effective budget shown in the UI is
|
||||
# max_context_tokens - reserved_tokens.
|
||||
max_context_tokens = 0
|
||||
reserved_tokens = 4096
|
||||
warning_threshold = 0.70
|
||||
error_threshold = 0.90
|
||||
critical_threshold = 0.95
|
||||
min_keep_recent_rounds = 3
|
||||
max_observation_age_rounds = 5
|
||||
truncated_observation_max_chars = 200
|
||||
min_keep_tokens = 10000
|
||||
max_compact_failures = 3
|
||||
|
||||
[service.web.database]
|
||||
type = "sqlite"
|
||||
path = "pilot/meta_data/dbgpt.db"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""ScheduledTaskServe - mount routes, start scheduler, recover jobs on boot."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from sqlalchemy import URL
|
||||
@@ -22,6 +23,35 @@ from .service.service import ScheduledTaskService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Env var name for the master switch that controls whether scheduled tasks
|
||||
# are actually *executed* on this process. See _scheduler_enabled().
|
||||
_SCHEDULER_ENABLED_ENV = "DBGPT_CHAT_TASK_SCHEDULER_ENABLED"
|
||||
|
||||
# In-memory jobstore sentinel. TaskScheduler treats this exact URL as a signal
|
||||
# to use APScheduler's MemoryJobStore (no SQLite file, no apscheduler_jobs
|
||||
# table). The business tables (dbgpt_serve_scheduled_task / _run) are the sole
|
||||
# source of truth; jobs are rehydrated into memory on boot via
|
||||
# _recover_jobs_from_db().
|
||||
_IN_MEMORY_JOBSTORE_URL = "sqlite:///:memory:"
|
||||
|
||||
|
||||
def _scheduler_enabled() -> bool:
|
||||
"""Env-driven master switch for *executing* scheduled tasks.
|
||||
|
||||
Controls rehydrate + triggering only — the REST API is always mounted,
|
||||
so task CRUD keeps working even when this returns False.
|
||||
|
||||
Reads ``DBGPT_CHAT_TASK_SCHEDULER_ENABLED``. Accepts ``1/true/yes/on``
|
||||
(case-insensitive) as enabled. Unset defaults to enabled.
|
||||
|
||||
Returns:
|
||||
bool: True if scheduled-task execution should run on this process.
|
||||
"""
|
||||
raw = os.getenv(_SCHEDULER_ENABLED_ENV)
|
||||
if raw is None:
|
||||
return True
|
||||
return raw.strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
class ScheduledTaskServe(BaseServe):
|
||||
"""Serve component for scheduled tasks.
|
||||
@@ -69,8 +99,13 @@ class ScheduledTaskServe(BaseServe):
|
||||
|
||||
# Build scheduler / service. The runner callable is the
|
||||
# module-level run_scheduled_task (not a bound method) so that
|
||||
# APScheduler's SQLAlchemyJobStore can pickle the job state.
|
||||
self._scheduler = TaskScheduler()
|
||||
# APScheduler can pickle the job state if ever needed.
|
||||
#
|
||||
# Use an in-memory jobstore: the business tables are the single source
|
||||
# of truth and jobs are rehydrated on boot (_recover_jobs_from_db),
|
||||
# so there is no need for APScheduler to persist its own job table
|
||||
# (avoids a stray scheduler.db / apscheduler_jobs table in prod DBs).
|
||||
self._scheduler = TaskScheduler(jobstore_url=_IN_MEMORY_JOBSTORE_URL)
|
||||
self._service = ScheduledTaskService(
|
||||
scheduler=self._scheduler,
|
||||
runner_callable=run_scheduled_task,
|
||||
@@ -95,7 +130,19 @@ class ScheduledTaskServe(BaseServe):
|
||||
event loop is already running — the correct context for
|
||||
AsyncIOScheduler.start(). The runner auth secret is resolved in
|
||||
init_app, so the scheduler always starts here.
|
||||
|
||||
Gated by DBGPT_CHAT_TASK_SCHEDULER_ENABLED: when disabled, the
|
||||
scheduler is neither started nor rehydrated, but the REST API
|
||||
(mounted in init_app) stays fully available.
|
||||
"""
|
||||
if not _scheduler_enabled():
|
||||
logger.info(
|
||||
"Scheduler execution disabled via %s; REST API stays "
|
||||
"available, no jobs will be rehydrated or triggered.",
|
||||
_SCHEDULER_ENABLED_ENV,
|
||||
)
|
||||
return
|
||||
|
||||
if self._scheduler is None:
|
||||
logger.warning("Scheduler not initialised; skipping startup.")
|
||||
return
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ChatEn = {
|
||||
use_skill: 'Use Skill',
|
||||
use_knowledge: 'Use Knowledge Base',
|
||||
use_database: 'Use Database',
|
||||
use_connector: 'Select Connector',
|
||||
use_connector: 'Select MCP',
|
||||
execution_steps: 'Execution Steps',
|
||||
db_gpt_computer: "DB-GPT's Computer",
|
||||
load_skill: 'Load Skill',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import MarkdownContext from '@/new-components/common/MarkdownContext';
|
||||
import { AttachedConnector } from '@/new-components/connector/types';
|
||||
import {
|
||||
ApiOutlined,
|
||||
AppstoreOutlined,
|
||||
@@ -36,7 +37,6 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from '
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ObservationFormatter from './ObservationFormatter';
|
||||
import TaskPlanCard, { TaskItem } from './TaskPlanCard';
|
||||
import { AttachedConnector } from '@/new-components/connector/types';
|
||||
|
||||
export type StepStatus = 'pending' | 'running' | 'completed' | 'error';
|
||||
|
||||
@@ -353,7 +353,7 @@ const SkillCompactCard: React.FC<{
|
||||
onDownload?: () => void;
|
||||
}> = memo(({ skillName, onClick, onDownload }) => {
|
||||
const { t } = useTranslation();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [downloading, _setDownloading] = useState(false);
|
||||
const [isAdded, setIsAdded] = useState(false);
|
||||
return (
|
||||
<div
|
||||
@@ -532,8 +532,9 @@ const StepCard: React.FC<{
|
||||
);
|
||||
}
|
||||
if (isTodoStep) {
|
||||
const progressText = getTodoStepBadge(t, step);
|
||||
const todoTitle = getTodoStepTitle(t, step);
|
||||
const tFn = t as (key: string, options?: Record<string, any>) => string;
|
||||
const progressText = getTodoStepBadge(tFn, step);
|
||||
const todoTitle = getTodoStepTitle(tFn, step);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -701,7 +702,6 @@ const SkillResourceCard: React.FC<{
|
||||
}> = memo(({ step, isActive, onClick }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [contentExpanded, setContentExpanded] = useState(false);
|
||||
const parsed = useMemo(() => parseSkillResourceDescription(step.description), [step.description]);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -714,7 +714,6 @@ const SkillResourceCard: React.FC<{
|
||||
}
|
||||
|
||||
const resourceName = parsed.resourcePath.split('/').pop() || parsed.resourcePath;
|
||||
const hasContent = parsed.content.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -803,7 +802,6 @@ const SectionBlock: React.FC<{
|
||||
defaultExpanded?: boolean;
|
||||
stepThoughts?: Record<string, string>;
|
||||
}> = memo(({ section, activeStepId, onStepClick, defaultExpanded = true, stepThoughts }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
|
||||
const completedCount = section.steps.filter(s => s.status === 'completed').length;
|
||||
@@ -811,10 +809,6 @@ const SectionBlock: React.FC<{
|
||||
const isAllCompleted = completedCount === totalCount && totalCount > 0;
|
||||
const hasRunningStep = section.steps.some(s => s.status === 'running');
|
||||
|
||||
const hasObservations = useMemo(() => {
|
||||
return section.steps.some(step => step.description?.includes('Observation:'));
|
||||
}, [section.steps]);
|
||||
|
||||
return (
|
||||
<div className='mb-4'>
|
||||
{/* Section Header */}
|
||||
@@ -898,7 +892,7 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
|
||||
onArtifactClick,
|
||||
onArtifactDownload,
|
||||
onViewAllFiles,
|
||||
onShare,
|
||||
onShare: _onShare,
|
||||
isCollapsed,
|
||||
onExpand,
|
||||
attachedFile,
|
||||
@@ -1019,7 +1013,10 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{(attachedConnectors ?? []).map(c => (
|
||||
<div key={c.id} className='flex items-center gap-2.5 px-3.5 py-2.5 rounded-xl border border-gray-200 dark:border-gray-700/60 bg-white dark:bg-[#1a1b1e] shadow-sm'>
|
||||
<div
|
||||
key={c.id}
|
||||
className='flex items-center gap-2.5 px-3.5 py-2.5 rounded-xl border border-gray-200 dark:border-gray-700/60 bg-white dark:bg-[#1a1b1e] shadow-sm'
|
||||
>
|
||||
<div className='w-8 h-8 rounded-lg bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center flex-shrink-0'>
|
||||
<ApiOutlined className='text-violet-500 text-base' />
|
||||
</div>
|
||||
@@ -1027,9 +1024,7 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
|
||||
<div className='text-sm font-medium text-gray-800 dark:text-gray-200 truncate'>
|
||||
{c.display_name}
|
||||
</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>
|
||||
{c.connector_type}
|
||||
</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>{c.connector_type}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -2564,40 +2564,6 @@ const Playground: NextPage = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Input Area at Bottom for Chat Mode - Premium Layered Style */}
|
||||
<div className='bg-gradient-to-t from-white via-white/95 to-white/80 dark:from-[#1a1b1e] dark:via-[#1a1b1e]/95 dark:to-[#1a1b1e]/80 p-4 md:p-6 pt-2'>
|
||||
<div className='max-w-[720px] mx-auto'>
|
||||
<div className='relative'>
|
||||
{/* Context Tags Area */}
|
||||
<div className='flex flex-wrap gap-2 mb-2'>
|
||||
{selectedDb && (
|
||||
<Tag
|
||||
closable
|
||||
onClose={() => setSelectedDb(null)}
|
||||
className='flex items-center gap-1 bg-blue-50 border-blue-200 text-blue-700 px-3 py-1 rounded-full'
|
||||
>
|
||||
{getDbIcon(selectedDb.type)} <span className='font-medium ml-1'>{selectedDb.db_name}</span>
|
||||
</Tag>
|
||||
)}
|
||||
{selectedKnowledge && (
|
||||
<Tag
|
||||
closable
|
||||
onClose={() => setSelectedKnowledge(null)}
|
||||
className='flex items-center gap-1 bg-orange-50 border-orange-200 text-orange-700 px-3 py-1 rounded-full'
|
||||
>
|
||||
<BookOutlined /> <span className='font-medium ml-1'>{selectedKnowledge.name}</span>
|
||||
</Tag>
|
||||
)}
|
||||
{uploadedFile && (
|
||||
<Tag
|
||||
closable
|
||||
onClose={() => setUploadedFile(null)}
|
||||
className='flex items-center gap-1 bg-green-50 border-green-200 text-green-700 px-3 py-1 rounded-full'
|
||||
>
|
||||
<FileExcelOutlined /> <span className='font-medium ml-1'>{uploadedFile.name}</span>
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
{/* Input Area at Bottom for Chat Mode - Hidden in read-only task replay mode */}
|
||||
{!router.query.from_task && (
|
||||
<div className='bg-gradient-to-t from-white via-white/95 to-white/80 dark:from-[#1a1b1e] dark:via-[#1a1b1e]/95 dark:to-[#1a1b1e]/80 p-4 md:p-6'>
|
||||
@@ -2679,76 +2645,7 @@ const Playground: NextPage = () => {
|
||||
className='flex-1 resize-none !border-none !shadow-none !bg-transparent px-0 py-2'
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
/>
|
||||
{/* Outer Frame - Floating Effect */}
|
||||
<div className='rounded-2xl w-full relative transition-all duration-300 shadow-[0_12px_32px_rgba(0,0,0,0.1),0_4px_12px_rgba(0,0,0,0.06)] hover:shadow-[0_20px_48px_rgba(0,0,0,0.16),0_8px_24px_rgba(0,0,0,0.08)] dark:shadow-[0_12px_32px_rgba(0,0,0,0.4)] dark:hover:shadow-[0_20px_48px_rgba(0,0,0,0.5)]'>
|
||||
{/* White Inner Box - Clean Glass Card */}
|
||||
<div className='bg-white/95 backdrop-blur-md dark:bg-[#1e1f24]/95 rounded-2xl border border-gray-100 dark:border-[#33353b] shadow-[inset_0_1px_0_rgba(255,255,255,1)] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] p-3 px-4'>
|
||||
<Input.TextArea
|
||||
value={query}
|
||||
onChange={e => {
|
||||
const newValue = e.target.value;
|
||||
setQuery(newValue);
|
||||
if (newValue === '/' && !isSkillPanelOpen && !selectedSkill) {
|
||||
setIsSkillPanelOpen(true);
|
||||
}
|
||||
}}
|
||||
onPressEnter={e => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleStart();
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
t('ask_data_question') ||
|
||||
'Ask a question about your database, upload a CSV, or generate a report...'
|
||||
}
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
className='flex-1 resize-none !border-none !shadow-none !bg-transparent px-0 py-2'
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
/>
|
||||
|
||||
{/* Toolbar Row */}
|
||||
<div className='flex items-center justify-between mt-1'>
|
||||
<div className='flex items-center gap-3'>
|
||||
{/* Add Button */}
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'upload',
|
||||
label: (
|
||||
<Upload {...uploadProps}>
|
||||
<div className='w-full'>Upload File</div>
|
||||
</Upload>
|
||||
),
|
||||
icon: <UploadOutlined />,
|
||||
},
|
||||
{
|
||||
key: 'database',
|
||||
label: 'Select Data Source',
|
||||
icon: <DatabaseOutlined />,
|
||||
onClick: () => setIsDbModalOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'knowledge',
|
||||
label: 'Select Knowledge Base',
|
||||
icon: <BookOutlined />,
|
||||
onClick: () => setIsKnowledgeModalOpen(true),
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Tooltip title={t('add_context')}>
|
||||
<Button
|
||||
type='text'
|
||||
shape='circle'
|
||||
size='small'
|
||||
icon={<PlusOutlined />}
|
||||
className='flex items-center justify-center text-gray-500 hover:text-violet-600 bg-gradient-to-b from-white to-gray-50 dark:from-[#2a2b2f] dark:to-[#1e1f24] dark:text-gray-300 border border-gray-200/80 dark:border-white/10 shadow-[0_1px_2px_rgba(0,0,0,0.05),inset_0_1px_0_rgba(255,255,255,1)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.2),inset_0_1px_0_rgba(255,255,255,0.05)] hover:-translate-y-[0.5px] hover:shadow-[0_2px_4px_rgba(0,0,0,0.06),inset_0_1px_0_rgba(255,255,255,1)] dark:hover:border-white/20 transition-all flex-shrink-0'
|
||||
/>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
{/* Toolbar Row */}
|
||||
<div className='flex items-center justify-between mt-1'>
|
||||
<div className='flex items-center gap-3'>
|
||||
@@ -2798,141 +2695,6 @@ const Playground: NextPage = () => {
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
|
||||
{/* Skill Selector Button with Badge */}
|
||||
<Popover
|
||||
trigger='click'
|
||||
placement='topLeft'
|
||||
open={isSkillPanelOpen}
|
||||
onOpenChange={setIsSkillPanelOpen}
|
||||
overlayClassName='manus-skill-menu'
|
||||
overlayInnerStyle={{ padding: 0, borderRadius: 12 }}
|
||||
content={
|
||||
<div className='w-[320px] bg-white dark:bg-[#2c2d31] rounded-xl shadow-xl overflow-hidden'>
|
||||
<div className='p-3 border-b border-gray-100 dark:border-gray-700'>
|
||||
<Input
|
||||
placeholder={t('search_skill')}
|
||||
prefix={<SearchOutlined className='text-gray-400' />}
|
||||
value={skillSearchQuery}
|
||||
onChange={e => setSkillSearchQuery(e.target.value)}
|
||||
className='rounded-lg'
|
||||
allowClear
|
||||
size='small'
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-[300px] overflow-y-auto'>
|
||||
{(skillsList || [])
|
||||
.filter(
|
||||
skill =>
|
||||
!skillSearchQuery ||
|
||||
skill.name.toLowerCase().includes(skillSearchQuery.toLowerCase()) ||
|
||||
skill.description.toLowerCase().includes(skillSearchQuery.toLowerCase()),
|
||||
)
|
||||
.map(skill => (
|
||||
<div
|
||||
key={skill.id}
|
||||
onClick={() => {
|
||||
if (selectedSkill?.id === skill.id) {
|
||||
setSelectedSkill(null);
|
||||
setQuery('');
|
||||
} else {
|
||||
setSelectedSkill(skill);
|
||||
setQuery(`/${skill.name} `);
|
||||
}
|
||||
setIsSkillPanelOpen(false);
|
||||
setSkillSearchQuery('');
|
||||
}}
|
||||
className={`flex items-start gap-3 px-3 py-2.5 cursor-pointer transition-all hover:bg-gray-50 dark:hover:bg-gray-800 ${
|
||||
selectedSkill?.id === skill.id ? 'bg-purple-50 dark:bg-purple-900/20' : ''
|
||||
}`}
|
||||
>
|
||||
<div className='flex-shrink-0 w-7 h-7 rounded-lg bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center text-white text-xs'>
|
||||
{skill.icon || <ThunderboltOutlined />}
|
||||
</div>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='font-medium text-sm text-gray-800 dark:text-gray-200'>
|
||||
{skill.name}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded ${
|
||||
skill.type === 'official'
|
||||
? 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{skill.type === 'official' ? '官方' : '个人'}
|
||||
</span>
|
||||
</div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-2'>
|
||||
{skill.description}
|
||||
</p>
|
||||
</div>
|
||||
{selectedSkill?.id === skill.id && (
|
||||
<CheckCircleFilled className='text-purple-500 flex-shrink-0 text-sm' />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(skillsList || []).filter(
|
||||
skill =>
|
||||
!skillSearchQuery ||
|
||||
skill.name.toLowerCase().includes(skillSearchQuery.toLowerCase()) ||
|
||||
skill.description.toLowerCase().includes(skillSearchQuery.toLowerCase()),
|
||||
).length === 0 && (
|
||||
<div className='text-center py-8 text-gray-400'>
|
||||
<ThunderboltOutlined className='text-2xl mb-2 opacity-50' />
|
||||
<div className='text-xs'>
|
||||
{skillSearchQuery ? '未找到匹配的技能' : '暂无可用技能'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='border-t border-gray-100 dark:border-gray-700 px-3 py-2 flex items-center justify-between bg-gray-50/50 dark:bg-gray-900/50'>
|
||||
<span className='text-[10px] text-gray-400'>
|
||||
{(skillsList || []).length} 个技能可用
|
||||
</span>
|
||||
<Button
|
||||
type='link'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
router.push('/construct/skills');
|
||||
setIsSkillPanelOpen(false);
|
||||
}}
|
||||
className='text-[10px] p-0 h-auto'
|
||||
>
|
||||
管理技能 →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip
|
||||
title={
|
||||
selectedSkill
|
||||
? t('skill_selected', { name: selectedSkill.name })
|
||||
: t('select_skill')
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type='text'
|
||||
shape='circle'
|
||||
size='small'
|
||||
className={`relative flex items-center justify-center flex-shrink-0 transition-all ${
|
||||
selectedSkill
|
||||
? 'bg-gradient-to-br from-[#a78bfa] to-[#7c3aed] text-white border border-transparent shadow-[0_2px_4px_rgba(139,92,246,0.3),inset_0_1px_0_rgba(255,255,255,0.3)] hover:-translate-y-[0.5px] hover:shadow-[0_4px_8px_rgba(139,92,246,0.4),inset_0_1px_0_rgba(255,255,255,0.3)]'
|
||||
: 'text-gray-500 hover:text-violet-600 bg-gradient-to-b from-white to-gray-50 dark:from-[#2a2b2f] dark:to-[#1e1f24] dark:text-gray-300 border border-gray-200/80 dark:border-white/10 shadow-[0_1px_2px_rgba(0,0,0,0.05),inset_0_1px_0_rgba(255,255,255,1)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.2),inset_0_1px_0_rgba(255,255,255,0.05)] hover:-translate-y-[0.5px] hover:shadow-[0_2px_4px_rgba(0,0,0,0.06),inset_0_1px_0_rgba(255,255,255,1)] dark:hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
<div className='relative'>
|
||||
<ThunderboltOutlined className={selectedSkill ? 'text-white' : ''} />
|
||||
{selectedSkill && (
|
||||
<span className='absolute -top-1.5 -right-1.5 bg-white text-[#7c3aed] text-[8px] rounded-full w-3.5 h-3.5 flex items-center justify-center font-bold shadow-sm ring-1 ring-[#7c3aed]/30'>
|
||||
1
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
{/* Skill Selector Button with Badge */}
|
||||
<Popover
|
||||
trigger='click'
|
||||
@@ -3071,8 +2833,6 @@ const Playground: NextPage = () => {
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
|
||||
{/* Separator dot */}
|
||||
<div className='w-px h-4 bg-gray-200 dark:bg-gray-700 mx-0.5' />
|
||||
{/* Connector Selector Button */}
|
||||
<Popover
|
||||
trigger='click'
|
||||
@@ -3275,17 +3035,6 @@ const Playground: NextPage = () => {
|
||||
className='flex-shrink-0 h-9 w-9 transition-all duration-200 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
/>
|
||||
</Tooltip>
|
||||
<div className='flex items-center gap-3'>
|
||||
{/* Voice Button */}
|
||||
<Tooltip title={t('voice_input')}>
|
||||
<Button
|
||||
type='text'
|
||||
shape='circle'
|
||||
icon={<AudioOutlined className='text-gray-500 text-[18px]' />}
|
||||
onClick={() => message.info(t('voice_input_coming_soon'))}
|
||||
className='flex-shrink-0 h-9 w-9 transition-all duration-200 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* Send Button with blue gradient + gloss animation */}
|
||||
<Button
|
||||
@@ -3329,48 +3078,6 @@ const Playground: NextPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Send Button with blue gradient + gloss animation */}
|
||||
<Button
|
||||
type='primary'
|
||||
shape='circle'
|
||||
icon={<ArrowUpOutlined />}
|
||||
onClick={() => handleStart()}
|
||||
disabled={(!query.trim() && !uploadedFile) || loading}
|
||||
loading={loading}
|
||||
className={`group/send relative overflow-hidden border-none shadow-lg flex-shrink-0 h-9 w-9 transition-all duration-200 ${
|
||||
query.trim() || uploadedFile
|
||||
? 'bg-gradient-to-br from-[#3b82f6] to-[#2563eb] hover:shadow-blue-300/40 hover:shadow-xl hover:scale-105'
|
||||
: 'bg-gray-200 text-gray-400'
|
||||
}`}
|
||||
style={
|
||||
query.trim() || uploadedFile
|
||||
? { background: 'linear-gradient(135deg, #3b82f6, #2563eb)' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{(query.trim() || uploadedFile) && (
|
||||
<span
|
||||
className='absolute inset-0 opacity-0 group-hover/send:opacity-100 transition-opacity duration-300 pointer-events-none'
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(105deg, transparent 40%, rgba(255,255,255,0.25) 45%, rgba(255,255,255,0.35) 50%, rgba(255,255,255,0.25) 55%, transparent 60%)',
|
||||
animation: 'glossSweepChat 1.8s ease-in-out infinite',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `@keyframes glossSweepChat { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Panel toggle handle — placed between panels to avoid overflow clipping */}
|
||||
<div className='relative z-20 flex-shrink-0'>
|
||||
|
||||
Reference in New Issue
Block a user