diff --git a/frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts b/frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts index cf21ef542..7fd56e6a4 100644 --- a/frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts +++ b/frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts @@ -239,11 +239,23 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { await page.waitForTimeout(500) console.log('Clicked close/skip button') } else { - // Press Escape to dismiss + // Press Escape multiple times to dismiss all tour steps + console.log('Pressing Escape to dismiss overlay...') + await page.keyboard.press('Escape') + await page.waitForTimeout(300) + // Press Escape again to ensure all steps are dismissed await page.keyboard.press('Escape') await page.waitForTimeout(500) console.log('Pressed Escape to dismiss overlay') } + + // Verify overlay is gone + if (await driverOverlay.isVisible({ timeout: 500 }).catch(() => false)) { + console.warn('Overlay still visible, trying to click outside') + // Click outside the overlay to dismiss it + await page.mouse.click(10, 10) + await page.waitForTimeout(500) + } } } catch (_error) { console.log('No onboarding tour found or already dismissed') @@ -252,6 +264,7 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { /** * Helper function to select the test team in the UI + * Updated to work with the new QuickAccessCards pagination design (removed "More" button) */ async function selectTestTeam(page: Page): Promise { try { @@ -267,39 +280,69 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { console.log('Saved initial page screenshot') // Strategy 1: Look for team card directly in QuickAccessCards - const teamCardButton = page.locator(`button:has-text("${TEST_TEAM_NAME}")`).first() - if (await teamCardButton.isVisible({ timeout: 3000 }).catch(() => false)) { - console.log('Found team card button directly, clicking...') - await teamCardButton.click() - await page.waitForTimeout(1000) - return true - } + // Note: Team cards are now div elements, not buttons + const quickAccessCards = page.locator('[data-tour="quick-access-cards"]') + if (await quickAccessCards.isVisible({ timeout: 3000 }).catch(() => false)) { + console.log('Found QuickAccessCards container') + + // Try to find the team card by text (cards are divs with team.name) + const teamCard = quickAccessCards.locator(`div:has-text("${TEST_TEAM_NAME}")`).first() + if (await teamCard.isVisible({ timeout: 2000 }).catch(() => false)) { + console.log('Found team card directly, clicking...') + // Dismiss tour before clicking + await dismissOnboardingTour(page) + await teamCard.click({ force: true }) + await page.waitForTimeout(1000) + return true + } - // Strategy 2: Look for QuickAccessCards "More" button and search for team - const moreButton = page.locator( - '[data-tour="quick-access-cards"] button:has-text("更多"), [data-tour="quick-access-cards"] button:has-text("More")' - ) - if (await moreButton.isVisible({ timeout: 3000 }).catch(() => false)) { - console.log('Found "More" button in QuickAccessCards') - // Use force click to bypass any remaining overlays - await moreButton.click({ force: true }) - await page.waitForTimeout(500) + // Strategy 1b: If not visible, try scrolling through pages using right arrow + console.log('Team card not visible, trying pagination...') + const rightArrow = quickAccessCards.locator('button[aria-label="Scroll right"]') + let attempts = 0 + const maxAttempts = 5 // Maximum number of pages to scroll through + + while (attempts < maxAttempts) { + // Check if right arrow exists and is visible + if (!(await rightArrow.isVisible({ timeout: 1000 }).catch(() => false))) { + console.log('No more pages to scroll') + break + } - // Search for the test team - const searchInput = page - .locator('input[placeholder*="搜索"], input[placeholder*="search" i]') - .first() - if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) { - await searchInput.fill(TEST_TEAM_NAME) + console.log(`Scrolling to next page (attempt ${attempts + 1})...`) + await rightArrow.click() await page.waitForTimeout(500) + + // Check if team card is now visible + if (await teamCard.isVisible({ timeout: 1000 }).catch(() => false)) { + console.log('Found team card after scrolling, clicking...') + // Dismiss tour before clicking + await dismissOnboardingTour(page) + await teamCard.click({ force: true }) + await page.waitForTimeout(1000) + return true + } + + attempts++ } + } - // Click on the team in the dropdown - const teamOption = page.locator(`[role="option"]:has-text("${TEST_TEAM_NAME}")`).first() + // Strategy 2: Try TeamSelectorButton in ChatInputControls (for new chat sessions) + // This button shows "智能体" or "Agent" with AgentIcon + const teamSelectorButton = page.locator( + 'button:has-text("智能体"), button:has-text("Agent")' + ).first() + if (await teamSelectorButton.isVisible({ timeout: 2000 }).catch(() => false)) { + console.log('Found TeamSelectorButton, clicking...') + await teamSelectorButton.click() + await page.waitForTimeout(500) + + // Look for the test team in the popover (uses role="button" instead of role="option") + const teamOption = page.locator(`[role="button"]:has-text("${TEST_TEAM_NAME}")`).first() if (await teamOption.isVisible({ timeout: 3000 }).catch(() => false)) { + console.log('Found team in TeamSelectorButton popover, selecting...') await teamOption.click() await page.waitForTimeout(1000) - console.log(`Selected team from More dropdown: ${TEST_TEAM_NAME}`) return true } } @@ -321,7 +364,7 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { } } - // Strategy 4: Direct click on team card if visible + // Strategy 4: Direct click on team card if visible anywhere on page const teamCard = page.locator(`text="${TEST_TEAM_NAME}"`).first() if (await teamCard.isVisible({ timeout: 3000 }).catch(() => false)) { await teamCard.click() @@ -361,7 +404,9 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { // Check if model selection is required if (buttonText?.includes('Please select') || buttonText?.includes('请选择模型')) { console.log('Model selection required, clicking selector...') - await modelSelectorButton.click() + // Dismiss tour before clicking + await dismissOnboardingTour(page) + await modelSelectorButton.click({ force: true }) await page.waitForTimeout(500) // Look for our test model in the dropdown @@ -567,8 +612,11 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { return } + // Dismiss any onboarding tour overlay before clicking input + await dismissOnboardingTour(page) + // For contentEditable elements, we need to click first, then type - await messageInput.click() + await messageInput.click({ force: true }) await page.keyboard.type('What is in this image?') // Step 5: Send message @@ -707,8 +755,11 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => { return } + // Dismiss any onboarding tour overlay before clicking input + await dismissOnboardingTour(page) + // For contentEditable elements, we need to click first, then type - await messageInput.click() + await messageInput.click({ force: true }) await page.keyboard.type('Describe this image') // Look for send button diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 6c7a2dcfb..fd3b5d581 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -10,43 +10,46 @@ :root { color-scheme: light; - /* ChatGPT Light Theme */ + /* Wegent Light Theme - Purple Primary */ --color-bg-base: 255 255 255; - --color-bg-surface: 249 249 249; + --color-bg-surface: 255 255 255; --color-bg-muted: 243 244 246; - --color-bg-hover: 229 231 235; - --color-border: 224 224 224; - --color-border-strong: 192 192 192; - --color-text-primary: 26 26 26; - --color-text-secondary: 102 102 102; - --color-text-muted: 160 160 160; + --color-bg-hover: 93 94 201 / 0.06; + --color-border: 228 228 228; + --color-border-strong: 200 200 200; + --color-border-light: 243 244 246; + --color-text-primary: 51 51 51; + --color-text-secondary: 99 99 99; + --color-text-muted: 147 147 147; --color-text-inverted: 255 255 255; --color-primary: 93 94 201; --color-primary-contrast: 255 255 255; --color-focus-ring: 93 94 201; - --color-scrollbar-track: 224 224 224; - --color-scrollbar-thumb: 192 192 192; + --color-scrollbar-track: 228 228 228; + --color-scrollbar-thumb: 200 200 200; --color-success: 34 197 94; --color-error: 239 68 68; --color-link: 93 94 201; - --color-code-bg: 246 248 250; + --color-code-bg: 243 244 246; --color-popover: 255 255 255; - --color-popover-foreground: 26 26 26; - --color-tooltip: 26 26 26; + --color-popover-foreground: 51 51 51; + --color-tooltip: 51 51 51; --color-tooltip-foreground: 255 255 255; - --shadow-popover: 0 12px 32px rgba(15, 23, 42, 0.12); + --shadow-popover: 0 12px 32px rgba(93, 94, 201, 0.12); + --shadow-sidebar: 0 4px 30px rgba(93, 94, 201, 0.1); --radius: 0.5rem; } [data-theme='dark'] { color-scheme: dark; - /* ChatGPT Dark Theme */ + /* Wegent Dark Theme - Purple Primary */ --color-bg-base: 14 15 15; --color-bg-surface: 26 28 28; --color-bg-muted: 33 36 36; - --color-bg-hover: 42 45 45; + --color-bg-hover: 118 119 218 / 0.1; --color-border: 42 45 45; --color-border-strong: 52 53 53; + --color-border-light: 42 45 45; --color-text-primary: 236 236 236; --color-text-secondary: 212 212 212; --color-text-muted: 160 160 160; @@ -65,6 +68,7 @@ --color-tooltip: 236 236 236; --color-tooltip-foreground: 14 15 15; --shadow-popover: 0 8px 24px rgba(0, 0, 0, 0.5); + --shadow-sidebar: 0 4px 30px rgba(0, 0, 0, 0.3); --radius: 0.5rem; } diff --git a/frontend/src/components/icons/AgentIcon.tsx b/frontend/src/components/icons/AgentIcon.tsx new file mode 100644 index 000000000..9b94727b0 --- /dev/null +++ b/frontend/src/components/icons/AgentIcon.tsx @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2025 Weibo, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * AgentIcon Component + * + * Custom SVG icon for agent/team representation. + * Features multiple people silhouettes representing a team. + */ + +import React from 'react' + +interface AgentIconProps { + className?: string +} + +export function AgentIcon({ className }: AgentIconProps) { + return ( + + + + + + + + + + + + ) +} + +export default AgentIcon diff --git a/frontend/src/components/icons/ModelIcon.tsx b/frontend/src/components/icons/ModelIcon.tsx new file mode 100644 index 000000000..9444aa142 --- /dev/null +++ b/frontend/src/components/icons/ModelIcon.tsx @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2025 Weibo, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * ModelIcon Component + * + * Custom SVG icon for AI model selection. + * Features a geometric diamond/cube design representing AI models. + */ + +import React from 'react' + +interface ModelIconProps { + className?: string +} + +export function ModelIcon({ className }: ModelIconProps) { + return ( + + + + + ) +} + +export default ModelIcon diff --git a/frontend/src/components/ui/action-button.tsx b/frontend/src/components/ui/action-button.tsx index 2b6593c35..311bb344a 100644 --- a/frontend/src/components/ui/action-button.tsx +++ b/frontend/src/components/ui/action-button.tsx @@ -12,6 +12,8 @@ interface ActionButtonProps { disabled?: boolean title?: string icon: React.ReactNode + /** Optional text label to display next to the icon */ + label?: string variant?: 'default' | 'outline' | 'loading' className?: string asChild?: boolean @@ -66,11 +68,20 @@ export function ActionButton({ disabled = false, title, icon, + label, variant = 'default', className = '', }: ActionButtonProps) { - // Base styles shared by all variants - const baseStyles = 'h-9 w-9 rounded-full flex-shrink-0' + // Determine if this is an icon-only button or has a label + const hasLabel = Boolean(label) + + // Base styles - different for icon-only vs with-label buttons + // Design spec: height 36px, border-radius 24px, border 1px #E4E4E4, bg white + // With label: padding 10px 12px 10px 10px, gap 4px + // Icon only: 36x36 circle with centered icon + const baseStyles = hasLabel + ? 'h-9 rounded-[24px] flex-shrink-0 pl-2.5 pr-3 py-2.5 gap-1 inline-flex items-center' + : 'h-9 w-9 rounded-full flex-shrink-0' if (variant === 'loading') { // Static loading state (non-clickable) @@ -79,24 +90,27 @@ export function ActionButton({ className={`relative ${baseStyles} flex items-center justify-center border border-border bg-base ${className}`} > {icon} + {label && {label}} ) } // Clickable button (default or outline) const buttonVariant = variant === 'outline' ? 'outline' : 'ghost' - const defaultClassName = variant === 'outline' ? '' : 'border border-border' + // No border for default variant, create clean flat button style + const defaultClassName = variant === 'outline' ? 'border border-border' : '' return ( ) } diff --git a/frontend/src/features/layout/components/UserFloatingMenu.tsx b/frontend/src/features/layout/components/UserFloatingMenu.tsx index 93ec5859a..504b03db4 100644 --- a/frontend/src/features/layout/components/UserFloatingMenu.tsx +++ b/frontend/src/features/layout/components/UserFloatingMenu.tsx @@ -95,13 +95,13 @@ export function UserFloatingMenu({ className = '' }: UserFloatingMenuProps) { onClick={handleToggleMenu} aria-expanded={isExpanded} aria-haspopup="true" - className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-muted transition-all duration-200 group" + className="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-muted transition-all duration-200 group" >
-
- +
+ {userDisplayName} {isAdmin && ( @@ -112,7 +112,7 @@ export function UserFloatingMenu({ className = '' }: UserFloatingMenuProps) { )}
diff --git a/frontend/src/features/projects/components/ProjectSection.tsx b/frontend/src/features/projects/components/ProjectSection.tsx index c1179a610..22d99be3b 100644 --- a/frontend/src/features/projects/components/ProjectSection.tsx +++ b/frontend/src/features/projects/components/ProjectSection.tsx @@ -111,7 +111,7 @@ export function ProjectSection({ onTaskSelect }: ProjectSectionProps) { } return ( -
+
{/* Section Header */}
diff --git a/frontend/src/features/tasks/components/AttachmentButton.tsx b/frontend/src/features/tasks/components/AttachmentButton.tsx index 256abc215..016dc44fe 100644 --- a/frontend/src/features/tasks/components/AttachmentButton.tsx +++ b/frontend/src/features/tasks/components/AttachmentButton.tsx @@ -138,11 +138,9 @@ export default function AttachmentButton({
} - className="border-border bg-base text-text-primary hover:bg-hover" />
diff --git a/frontend/src/features/tasks/components/CorrectionModeToggle.tsx b/frontend/src/features/tasks/components/CorrectionModeToggle.tsx index f1dc63dd1..3c07b9ff8 100644 --- a/frontend/src/features/tasks/components/CorrectionModeToggle.tsx +++ b/frontend/src/features/tasks/components/CorrectionModeToggle.tsx @@ -151,15 +151,15 @@ export default function CorrectionModeToggle({
} + label={t('chat:correction.label')} className={cn( 'transition-colors', enabled - ? 'border-primary bg-primary/10 text-primary hover:bg-primary/20' - : 'border-border bg-base text-text-primary hover:bg-hover' + ? 'bg-primary/10 text-primary hover:bg-primary/20' + : 'text-text-primary hover:bg-hover' )} />
diff --git a/frontend/src/features/tasks/components/chat/AddContextButton.tsx b/frontend/src/features/tasks/components/chat/AddContextButton.tsx index 0800e89ce..f8b620499 100644 --- a/frontend/src/features/tasks/components/chat/AddContextButton.tsx +++ b/frontend/src/features/tasks/components/chat/AddContextButton.tsx @@ -5,6 +5,7 @@ 'use client' import React from 'react' +import { BookOpenText } from 'lucide-react' import { ActionButton } from '@/components/ui/action-button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { useTranslation } from '@/hooks/useTranslation' @@ -14,9 +15,9 @@ interface AddContextButtonProps { } /** - * Add Context Button - Icon-only button that opens knowledge base selector - * Always displays "#" symbol with tooltip on hover - * Uses ActionButton for consistent 36px size with other control buttons + * Add Context Button - Button with icon and label that opens knowledge base selector + * Displays BookOpenText icon with "知识库" label + * Uses ActionButton for consistent styling with other control buttons */ export default function AddContextButton({ onClick }: AddContextButtonProps) { const { t } = useTranslation() @@ -27,11 +28,10 @@ export default function AddContextButton({ onClick }: AddContextButtonProps) {
#} + icon={} + label={t('knowledge:tooltip')} title={t('knowledge:tooltip')} - className="border-border bg-base text-text-primary hover:bg-hover" />
diff --git a/frontend/src/features/tasks/components/chat/ChatArea.tsx b/frontend/src/features/tasks/components/chat/ChatArea.tsx index 0498b3522..a48c298c7 100644 --- a/frontend/src/features/tasks/components/chat/ChatArea.tsx +++ b/frontend/src/features/tasks/components/chat/ChatArea.tsx @@ -171,15 +171,15 @@ function ChatAreaContent({ // Derive available options and defaults from selected video model's config const videoConfig = videoModelSelection.selectedModel?.config?.videoConfig as | { - resolution?: string - ratio?: string - duration?: number - capabilities?: { - aspect_ratios?: { value: string }[] - resolutions?: { label: string }[] - durations_sec?: number[] - } + resolution?: string + ratio?: string + duration?: number + capabilities?: { + aspect_ratios?: { value: string }[] + resolutions?: { label: string }[] + durations_sec?: number[] } + } | undefined const videoCapabilities = videoConfig?.capabilities @@ -863,6 +863,7 @@ function ChatAreaContent({ taskInputMessage: chatState.taskInputMessage, setTaskInputMessage: chatState.setTaskInputMessage, selectedTeam: chatState.selectedTeam, + teams: teams, externalApiParams: chatState.externalApiParams, onTeamChange: chatState.handleTeamChange, onExternalApiParamsChange: chatState.handleExternalApiParamsChange, @@ -1050,10 +1051,10 @@ function ChatAreaContent({
{taskType !== 'knowledge' && } @@ -1092,9 +1093,10 @@ function ChatAreaContent({ > {/* Bottom gradient fade effect - text fades as it approaches the input, limited width to avoid overlapping scrollbar */}
scrollToBottom(true)} />
-
- +
+
+ +
)} diff --git a/frontend/src/features/tasks/components/chat/QuickAccessCards.tsx b/frontend/src/features/tasks/components/chat/QuickAccessCards.tsx index 7446d0c59..b62c987b2 100644 --- a/frontend/src/features/tasks/components/chat/QuickAccessCards.tsx +++ b/frontend/src/features/tasks/components/chat/QuickAccessCards.tsx @@ -4,31 +4,21 @@ 'use client' -import { useEffect, useState, useCallback, useRef, useMemo } from 'react' -import { useRouter } from 'next/navigation' -import { - ChevronDownIcon, - Cog6ToothIcon, - CheckIcon, - MagnifyingGlassIcon, - SparklesIcon, -} from '@heroicons/react/24/outline' -import { Wand2 } from 'lucide-react' +import { useEffect, useState, useCallback, useRef } from 'react' +import { ChevronLeftIcon, ChevronRightIcon, SparklesIcon } from '@heroicons/react/24/outline' import { userApis } from '@/apis/user' import { QuickAccessTeam, Team } from '@/types/api' import { useTranslation } from '@/hooks/useTranslation' -import { Tag } from '@/components/ui/tag' -import { Button } from '@/components/ui/button' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { paths } from '@/config/paths' -import { getSharedTagStyle as getSharedBadgeStyle } from '@/utils/styles' -import { TeamIconDisplay } from '@/features/settings/components/teams/TeamIconDisplay' -import TeamCreationWizard from '@/features/settings/components/wizard/TeamCreationWizard' -import { useMediaQuery } from '@/hooks/useMediaQuery' -import { MobileTeamSelector } from '@/features/tasks/components/selector' - -// Maximum number of quick access cards to display -const MAX_QUICK_ACCESS_CARDS = 4 + +// Container dimensions +const CONTAINER_WIDTH = 880 +const CONTAINER_HEIGHT = 108 + +// Card dimensions +const CARD_WIDTH = 154 +const CARD_GAP = 12 +const CARDS_PER_PAGE = 5 +const PAGE_SCROLL_AMOUNT = CARDS_PER_PAGE * CARD_WIDTH + (CARDS_PER_PAGE - 1) * CARD_GAP interface QuickAccessCardsProps { teams: Team[] @@ -37,10 +27,10 @@ interface QuickAccessCardsProps { currentMode: 'chat' | 'code' | 'knowledge' | 'task' | 'video' | 'image' isLoading?: boolean isTeamsLoading?: boolean - hideSelected?: boolean // Whether to hide the selected team from the cards + hideSelected?: boolean onRefreshTeams?: () => Promise - showWizardButton?: boolean // Whether to show the wizard button (only for chat mode) - defaultTeam?: Team | null // The default team for current mode (will be hidden from quick access cards) + showWizardButton?: boolean + defaultTeam?: Team | null } export function QuickAccessCards({ @@ -49,26 +39,22 @@ export function QuickAccessCards({ onTeamSelect, currentMode, isLoading, - isTeamsLoading, - hideSelected = false, - onRefreshTeams, - showWizardButton = false, + isTeamsLoading: _isTeamsLoading, + hideSelected: _hideSelected = false, + onRefreshTeams: _onRefreshTeams, + showWizardButton: _showWizardButton = false, defaultTeam, }: QuickAccessCardsProps) { - const router = useRouter() - const { t } = useTranslation(['common', 'wizard']) + const { t } = useTranslation('common') const [quickAccessTeams, setQuickAccessTeams] = useState([]) const [isQuickAccessLoading, setIsQuickAccessLoading] = useState(true) const [clickedTeamId, setClickedTeamId] = useState(null) - const [showMoreTeams, setShowMoreTeams] = useState(false) - const [showWizard, setShowWizard] = useState(false) - const moreButtonRef = useRef(null) - const isMobile = useMediaQuery('(max-width: 767px)') + const scrollContainerRef = useRef(null) + const [canScrollLeft, setCanScrollLeft] = useState(false) + const [canScrollRight, setCanScrollRight] = useState(false) - // Define the extended team type for display type DisplayTeam = Team & { is_system: boolean; recommended_mode?: 'chat' | 'code' | 'both' } - // Fetch quick access teams useEffect(() => { const fetchQuickAccess = async () => { try { @@ -77,7 +63,6 @@ export function QuickAccessCards({ setQuickAccessTeams(response.teams) } catch (error) { console.error('Failed to fetch quick access teams:', error) - // Fallback: use first few teams from the teams list setQuickAccessTeams([]) } finally { setIsQuickAccessLoading(false) @@ -87,15 +72,17 @@ export function QuickAccessCards({ fetchQuickAccess() }, []) - // Filter teams by bind_mode based on current mode (same logic as TeamSelector) + // Filter teams by bind_mode based on current mode const filteredTeams = teams.filter(team => { - // If bind_mode is not set or is an empty array, the team supports all modes - if (!team.bind_mode || team.bind_mode.length === 0) return true - // Only show if current mode is in bind_mode + // Filter out teams with empty bind_mode array + if (Array.isArray(team.bind_mode) && team.bind_mode.length === 0) return false + // If bind_mode is not set (undefined/null), show in all modes + if (!team.bind_mode) return true + // Otherwise, only show if current mode is in bind_mode return team.bind_mode.includes(currentMode) }) - // Get display teams: quick access teams matched with full team data + // Get all quick access teams matched with full team data const allDisplayTeams: DisplayTeam[] = quickAccessTeams.length > 0 ? quickAccessTeams @@ -114,82 +101,56 @@ export function QuickAccessCards({ : // Fallback: show first teams from filtered list if no quick access configured filteredTeams.map(t => ({ ...t, is_system: false }) as DisplayTeam) - // Filter out selected team if hideSelected is true, and always filter out default team - const teamsAfterFilter = allDisplayTeams.filter(t => { - // Always hide default team from quick access cards + // Filter out default team only (keep selected team visible with selection state) + const displayTeams = allDisplayTeams.filter(t => { if (defaultTeam && t.id === defaultTeam.id) return false - // Hide selected team if hideSelected is true - if (hideSelected && selectedTeam && t.id === selectedTeam.id) return false return true }) - // Limit display teams to MAX_QUICK_ACCESS_CARDS - const displayTeams = teamsAfterFilter.slice(0, MAX_QUICK_ACCESS_CARDS) + const needsPagination = displayTeams.length > CARDS_PER_PAGE + + const checkScrollState = useCallback(() => { + const container = scrollContainerRef.current + if (!container) return + + setCanScrollLeft(container.scrollLeft > 0) + setCanScrollRight(container.scrollLeft < container.scrollWidth - container.clientWidth - 1) + }, []) - // Close dropdown when clicking outside useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (moreButtonRef.current && !moreButtonRef.current.contains(event.target as Node)) { - setShowMoreTeams(false) - } - } + const container = scrollContainerRef.current + if (!container) return - if (showMoreTeams) { - document.addEventListener('mousedown', handleClickOutside) - } + checkScrollState() + container.addEventListener('scroll', checkScrollState, { passive: true }) + window.addEventListener('resize', checkScrollState) return () => { - document.removeEventListener('mousedown', handleClickOutside) + container.removeEventListener('scroll', checkScrollState) + window.removeEventListener('resize', checkScrollState) } - }, [showMoreTeams]) + }, [checkScrollState, displayTeams]) - // Handle team selection from dropdown - const handleTeamSelectFromDropdown = useCallback( - (team: Team) => { - onTeamSelect(team) - setShowMoreTeams(false) - }, - [onTeamSelect] - ) - - // Search state for team list - const [searchQuery, setSearchQuery] = useState('') - - // Filter teams for dropdown based on search query (excluding default team) - const dropdownTeams = useMemo(() => { - // First filter out the default team - const teamsWithoutDefault = filteredTeams.filter(team => { - if (defaultTeam && team.id === defaultTeam.id) return false - return true - }) - - if (!searchQuery.trim()) return teamsWithoutDefault - const query = searchQuery.toLowerCase() - return teamsWithoutDefault.filter(team => team.name.toLowerCase().includes(query)) - }, [filteredTeams, searchQuery, defaultTeam]) - - // Get shared badge style - const sharedBadgeStyle = useMemo(() => getSharedBadgeStyle(), []) + const scrollLeft = () => { + const container = scrollContainerRef.current + if (!container) return + container.scrollBy({ left: -PAGE_SCROLL_AMOUNT, behavior: 'smooth' }) + } - // Reset search when dropdown closes - useEffect(() => { - if (!showMoreTeams) { - setSearchQuery('') - } - }, [showMoreTeams]) + const scrollRight = () => { + const container = scrollContainerRef.current + if (!container) return + container.scrollBy({ left: PAGE_SCROLL_AMOUNT, behavior: 'smooth' }) + } - // Handle team click - simply select the team with animation const handleTeamClick = useCallback( (team: DisplayTeam) => { - // Trigger click animation setClickedTeamId(team.id) - // Select the team after animation starts setTimeout(() => { onTeamSelect(team) }, 150) - // Reset the clicked state after animation completes setTimeout(() => { setClickedTeamId(null) }, 300) @@ -199,146 +160,93 @@ export function QuickAccessCards({ if (isLoading || isQuickAccessLoading) { return ( -
- {[1, 2, 3].map(i => ( -
-
-
-
- ))} +
+
+ {[1, 2, 3, 4, 5].map(i => ( +
+ ))} +
) } - // Only show empty state when user truly has no teams available for current mode - // Don't show it when teams exist but are just filtered out (e.g., default team hidden) - if (filteredTeams.length === 0) { - // Show empty state guidance card when no teams are available + if (teams.length === 0) { return ( - <> -
- {/* Empty state guidance card */} -
-
-
- -
+
+
+
+
+
-

- {t('teams.no_teams_title')} -

-

{t('teams.no_teams_description')}

-
+

+ {t('teams.no_teams_title')} +

+

{t('teams.no_teams_description')}

- - {/* Team Creation Wizard Dialog */} - setShowWizard(false)} - onSuccess={async (teamId, _) => { - // Refresh teams list first to get the new team - if (onRefreshTeams) { - const refreshedTeams = await onRefreshTeams() - // Find and select the new team from refreshed list - const newTeam = refreshedTeams.find(t => t.id === teamId) - if (newTeam) { - onTeamSelect(newTeam) - } - } else { - // Fallback: try to find in current teams (may not work for newly created) - const newTeam = teams.find(t => t.id === teamId) - if (newTeam) { - onTeamSelect(newTeam) - } - } - }} - /> - +
) } - // Helper function to check if a team is a personal team (not public, not group) - const isPersonalTeam = (team: DisplayTeam) => { - const isPublic = 'user_id' in team && team.user_id === 0 - const isGroup = team.namespace && team.namespace !== 'default' - return !isPublic && !isGroup - } - - // Helper function to check if a team is a group team - const isGroupTeam = (team: DisplayTeam) => { - return team.namespace && team.namespace !== 'default' + // Don't show quick access cards if no teams are available after filtering + if (displayTeams.length === 0) { + return null } - // Render a single team card with optional tooltip const renderTeamCard = (team: DisplayTeam) => { const isSelected = selectedTeam?.id === team.id const isClicked = clickedTeamId === team.id + const description = team.description || t('teams.no_description') - const cardContent = ( + return (
!isClicked && handleTeamClick(team)} className={` - group relative flex items-center gap-1 h-[42px] px-4 - rounded-full border cursor-pointer transition-all duration-200 + group relative flex flex-col justify-center + cursor-pointer transition-all duration-200 ${ - isClicked - ? 'clicking-card border-primary bg-primary/10 ring-2 ring-primary/50' - : isSelected - ? 'border-primary bg-primary/5' - : 'border-border bg-base hover:bg-hover hover:border-border-strong hover:shadow-sm' + isSelected + ? 'border-l-[3px] border-l-primary border-y border-r border-border bg-primary/5' + : 'border border-border bg-base' } + ${isClicked ? 'clicking-card' : ''} ${isClicked ? 'pointer-events-none' : ''} + ${!isSelected ? 'hover:shadow-[0_2px_12px_0_rgba(0,0,0,0.1)]' : ''} `} + style={{ + width: CARD_WIDTH, + height: 78, + padding: '8px 12px', + borderRadius: 20, + flexShrink: 0, + flexGrow: 0, + }} > - - - {team.name} - - - {/* Personal or Group badge */} - {isPersonalTeam(team) && ( - - {t('settings.personal')} - - )} - {isGroupTeam(team) && ( - - {team.namespace} - - )} -
- ) - - // Tooltip content: prioritize description, fallback to name - const tooltipText = team.description || team.name +
+ + {team.name} + +
- // Always wrap with Tooltip - return ( - - - {cardContent} - -

{tooltipText}

-
-
-
+

{description}

+
) } @@ -374,226 +282,68 @@ export function QuickAccessCards({ pulse-glow 0.3s ease-out, scale-bounce 0.3s ease-out; } + + .hide-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } + + .hide-scrollbar::-webkit-scrollbar { + display: none; + } `} -
- {/* Show selected team first with highlighted style (only if not the default team) */} - {selectedTeam && (!defaultTeam || selectedTeam.id !== defaultTeam.id) && ( -
- - {selectedTeam.name} -
- )} - {displayTeams.map(team => renderTeamCard(team))} - - {/* More button - use MobileTeamSelector on mobile, dropdown on desktop */} - {isMobile ? ( - // Mobile: Use iOS-style drawer selector with "更多" text - dropdownTeams.length > 0 && selectedTeam ? ( - - ) : ( - // Fallback: Show a button with "更多" text if no team selected but teams exist - filteredTeams.length > 0 && ( - - ) - ) - ) : ( - // Desktop: Original dropdown -
+ +
+
+ {needsPagination && canScrollLeft && ( + )} - {/* Dropdown with team list */} - {showMoreTeams && ( -
- {/* Search input */} -
-
- - setSearchQuery(e.target.value)} - placeholder={t('teams.search_team')} - className="w-full pl-9 pr-3 py-2 text-sm bg-base border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 placeholder:text-text-muted" - autoFocus - /> -
-
- - {/* Team list */} -
- {isTeamsLoading ? ( -
- {t('actions.loading')} -
- ) : dropdownTeams.length === 0 ? ( -
- {t('teams.no_match')} -
- ) : ( - dropdownTeams.map(team => { - const isSelected = selectedTeam?.id === team.id - const isSharedTeam = team.share_status === 2 && team.user?.user_name - const isGroupTeamItem = team.namespace && team.namespace !== 'default' - const isPublicTeam = 'user_id' in team && team.user_id === 0 - const isPersonalTeamItem = !isPublicTeam && !isGroupTeamItem - - return ( -
handleTeamSelectFromDropdown(team)} - className={` - flex items-center gap-3 px-3 py-2 mx-1 my-0.5 rounded-md cursor-pointer - transition-colors duration-150 - ${ - isSelected - ? 'bg-primary/10 text-primary' - : 'hover:bg-hover text-text-primary' - } - `} - > - - - - {team.name} - - {isPersonalTeamItem && ( - - {t('settings.personal')} - - )} - {isGroupTeamItem && ( - - {team.namespace} - - )} - {isSharedTeam && ( - - {team.user?.user_name} - - )} -
- ) - }) - )} -
- - {/* Footer with settings link */} -
{ - setShowMoreTeams(false) - router.push(paths.settings.team.getHref()) - }} - > - - - {t('teams.manage')} - -
-
- )} +
+ {displayTeams.map(team => ( +
{renderTeamCard(team)}
+ ))}
- )} - - {/* Divider */} -
- - {/* Wizard button - quick create agent (only show in chat mode) */} - {showWizardButton && ( - - - - - - -

{t('wizard:wizard_button_tooltip')}

-
-
-
- )} -
- {/* Team Creation Wizard Dialog */} - {showWizardButton && ( - setShowWizard(false)} - onSuccess={async (teamId, _) => { - // Refresh teams list first to get the new team - if (onRefreshTeams) { - const refreshedTeams = await onRefreshTeams() - // Find and select the new team from refreshed list - const newTeam = refreshedTeams.find(t => t.id === teamId) - if (newTeam) { - onTeamSelect(newTeam) - } - } else { - // Fallback: try to find in current teams (may not work for newly created) - const newTeam = teams.find(t => t.id === teamId) - if (newTeam) { - onTeamSelect(newTeam) - } - } - }} - /> - )} + {needsPagination && canScrollRight && ( + + )} +
+
) } diff --git a/frontend/src/features/tasks/components/clarification/ClarificationToggle.tsx b/frontend/src/features/tasks/components/clarification/ClarificationToggle.tsx index 6a219b919..35fa0f0b6 100644 --- a/frontend/src/features/tasks/components/clarification/ClarificationToggle.tsx +++ b/frontend/src/features/tasks/components/clarification/ClarificationToggle.tsx @@ -40,15 +40,15 @@ export default function ClarificationToggle({
} + label={t('chat:clarification_toggle.label')} className={cn( 'transition-colors', enabled - ? 'border-primary bg-primary/10 text-primary hover:bg-primary/20' - : 'border-border bg-base text-text-primary hover:bg-hover' + ? 'bg-primary/10 text-primary hover:bg-primary/20' + : 'text-text-primary hover:bg-hover' )} />
diff --git a/frontend/src/features/tasks/components/input/ChatInputCard.tsx b/frontend/src/features/tasks/components/input/ChatInputCard.tsx index 348ad33b3..321cc5579 100644 --- a/frontend/src/features/tasks/components/input/ChatInputCard.tsx +++ b/frontend/src/features/tasks/components/input/ChatInputCard.tsx @@ -27,6 +27,8 @@ export interface ChatInputCardProps extends Omit< // Team and external API selectedTeam: Team | null + /** Available teams for team selector */ + teams?: Team[] externalApiParams: Record onExternalApiParamsChange: (params: Record) => void onAppModeChange: (mode: string | undefined) => void @@ -92,6 +94,7 @@ export function ChatInputCard({ taskInputMessage, setTaskInputMessage, selectedTeam, + teams = [], onTeamChange, externalApiParams, onExternalApiParamsChange, @@ -214,22 +217,22 @@ export function ChatInputCard({ {/* Chat Input Card */}
- {/* Drag Overlay */} - {isDragging && ( -
-
- + {isDragging && ( +
+
+ +
+

释放以上传文件

+

支持 PDF, Word, TXT, Markdown 等格式

-

释放以上传文件

-

支持 PDF, Word, TXT, Markdown 等格式

-
- )} + )} {/* Unified Badge Display - Knowledge bases and attachments */} +
void selectedModel: Model | null setSelectedModel: (model: Model | null) => void @@ -166,6 +169,7 @@ export interface ChatInputControlsProps { export function ChatInputControls({ taskType, selectedTeam, + teams = [], onTeamChange: _onTeamChange, selectedModel, setSelectedModel, @@ -366,10 +370,10 @@ export function ChatInputControls({ // Desktop layout: original full layout return (
{/* Generate Mode Selector - show when in video or image mode */} @@ -457,20 +461,33 @@ export function ChatInputControls({ {/* Non-generation mode controls (chat, code, etc.) */} {!isGenerationMode && ( <> - {/* Context Selection - only show for chat shell */} - {isChatShell(selectedTeam) && ( - - )} - {/* File Upload Button - show for shells that support attachments (Chat, ClaudeCode) */} {supportsAttachments(selectedTeam) && ( )} + {/* Divider between attachment and other controls */} + {supportsAttachments(selectedTeam) && selectedTeam && ( +
+ )} + + {/* Team Selector - show when teams are available, onTeamChange is provided, and no messages yet */} + {teams.length > 0 && _onTeamChange && !hasMessages && ( + { + if (team) { + _onTeamChange(team) + } + }} + teams={teams} + disabled={isLoading || isStreaming} + taskDetail={selectedTaskDetail} + hideSettingsLink={true} + currentMode={taskType} + /> + )} + {/* Skill Selector - show when skills are available */} {/* Skill selection is read-only after task creation (hasMessages) */} {availableSkills.length > 0 && onToggleSkill && ( @@ -487,6 +504,15 @@ export function ChatInputControls({ /> )} + {/* Context Selection - only show for chat shell */} + {isChatShell(selectedTeam) && ( + + )} + {/* Clarification Toggle Button - only show for chat shell */} {isChatShell(selectedTeam) && ( )} - {/* Model Selector */} + {/* Model Selector - placed at the end of left side buttons */} {selectedTeam && ( )} - {/* Deep Thinking Toggle Button - hidden for now */} - {/* {isChatShell(selectedTeam) && ( - - )} */} - {/* Send/Stop Button */} {renderSendButton()}
diff --git a/frontend/src/features/tasks/components/input/SendButton.tsx b/frontend/src/features/tasks/components/input/SendButton.tsx index 200bd49a0..03b2f5468 100644 --- a/frontend/src/features/tasks/components/input/SendButton.tsx +++ b/frontend/src/features/tasks/components/input/SendButton.tsx @@ -4,13 +4,8 @@ 'use client' -import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react' -import { Send, ChevronDown, Check } from 'lucide-react' -import { useTranslation } from '@/hooks/useTranslation' -import { useUser } from '@/features/common/UserContext' -import { userApis } from '@/apis/user' -import { useToast } from '@/hooks/use-toast' -import type { UserPreferences } from '@/types/api' +import React, { useRef, useCallback } from 'react' +import { ArrowUp } from 'lucide-react' import LoadingDots from '../message/LoadingDots' interface SendButtonProps { @@ -18,82 +13,18 @@ interface SendButtonProps { disabled?: boolean isLoading?: boolean className?: string - /** Hide dropdown toggle for mobile */ + /** @deprecated No longer used, kept for API compatibility */ compact?: boolean } -type SendKeyOption = 'enter' | 'cmd_enter' - export default function SendButton({ onClick, disabled = false, isLoading = false, className = '', - compact = false, }: SendButtonProps) { - const { t } = useTranslation() - const { toast } = useToast() - const { user, refresh } = useUser() - const [isDropdownOpen, setIsDropdownOpen] = useState(false) - const [isSaving, setIsSaving] = useState(false) - const dropdownRef = useRef(null) const buttonRef = useRef(null) - // Get current send key preference from user context - const sendKey: SendKeyOption = (user?.preferences?.send_key as SendKeyOption) || 'enter' - - // Detect if Mac or Windows for display - const isMac = useMemo(() => { - return typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform) - }, []) - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) && - buttonRef.current && - !buttonRef.current.contains(event.target as Node) - ) { - setIsDropdownOpen(false) - } - } - - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, []) - - // Handle send key change - const handleSendKeyChange = useCallback( - async (value: SendKeyOption) => { - if (value === sendKey) { - setIsDropdownOpen(false) - return - } - - setIsSaving(true) - try { - const preferences: UserPreferences = { send_key: value } - await userApis.updateUser({ preferences }) - await refresh() - toast({ - title: t('chat:send_button.preference_saved'), - }) - } catch (error) { - console.error('Failed to save send key preference:', error) - toast({ - variant: 'destructive', - title: t('chat:send_button.preference_save_failed'), - }) - } finally { - setIsSaving(false) - setIsDropdownOpen(false) - } - }, - [sendKey, refresh, toast, t] - ) - // Handle main button click (send message) const handleMainClick = useCallback( (e: React.MouseEvent) => { @@ -106,136 +37,26 @@ export default function SendButton({ [disabled, isLoading, onClick] ) - // Handle dropdown toggle click - const handleDropdownToggle = useCallback( - (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - if (!isSaving) { - setIsDropdownOpen(prev => !prev) - } - }, - [isSaving] - ) - - // Get shortcut display text - const getShortcutText = useCallback( - (option: SendKeyOption): string => { - if (option === 'enter') { - return 'Enter' - } - return isMac ? '⌘ Enter' : 'Ctrl Enter' - }, - [isMac] - ) - - // Get option label - const getOptionLabel = useCallback( - (option: SendKeyOption): string => { - if (option === 'enter') { - return t('chat:send_button.option_enter') - } - return isMac - ? t('chat:send_button.option_cmd_enter_mac') - : t('chat:send_button.option_cmd_enter_win') - }, - [isMac, t] - ) - return (
- {/* Main button container with pill shape - 36px height to match Figma */} -
- {/* Send button - icon only */} - - - {/* Divider and Dropdown toggle - hidden in compact mode */} - {!compact && ( - <> -
- - - )} -
- - {/* Dropdown menu */} - {isDropdownOpen && !compact && ( -
-
-
- {t('chat:send_button.shortcut_title')} -
- {(['enter', 'cmd_enter'] as SendKeyOption[]).map(option => ( - - ))} -
-
- )} + {/* Send button - circular with larger icon */} +
) } diff --git a/frontend/src/features/tasks/components/selector/ModelSelector.tsx b/frontend/src/features/tasks/components/selector/ModelSelector.tsx index b4c5e2b76..715f934cf 100644 --- a/frontend/src/features/tasks/components/selector/ModelSelector.tsx +++ b/frontend/src/features/tasks/components/selector/ModelSelector.tsx @@ -20,8 +20,9 @@ import React, { useState, useEffect, useMemo } from 'react' import { Cog6ToothIcon } from '@heroicons/react/24/outline' -import { Check, Brain, ChevronDown, Video, ImageIcon } from 'lucide-react' +import { Check, ChevronDown, Video, ImageIcon } from 'lucide-react' import { useRouter } from 'next/navigation' +import { ModelIcon } from '@/components/icons/ModelIcon' import { Checkbox } from '@/components/ui/checkbox' import { useTranslation } from '@/hooks/useTranslation' import { useMediaQuery } from '@/hooks/useMediaQuery' @@ -145,7 +146,7 @@ export default function ModelSelector({ case 'image': return ImageIcon default: - return Brain + return ModelIcon } }, [modelCategoryType]) @@ -245,11 +246,11 @@ export default function ModelSelector({ aria-controls="model-selector-popover" disabled={isDisabled} className={cn( - 'flex items-center gap-1 min-w-0 rounded-full pl-2.5 pr-3 py-2.5 h-9', - 'border transition-colors', + 'flex items-center gap-1 min-w-0 rounded-[24px] pl-2.5 pr-3 py-2.5 h-9', + 'transition-colors', modelSelection.isModelRequired - ? 'border-error text-error bg-error/5 hover:bg-error/10' - : 'border-border bg-base text-text-primary hover:bg-hover', + ? 'border border-error text-error bg-error/5 hover:bg-error/10' + : 'bg-transparent text-text-primary hover:bg-hover', modelSelection.isLoading || externalLoading ? 'animate-pulse' : '', 'focus:outline-none focus:ring-0', 'disabled:cursor-not-allowed disabled:opacity-50' diff --git a/frontend/src/features/tasks/components/selector/SkillSelectorPopover.tsx b/frontend/src/features/tasks/components/selector/SkillSelectorPopover.tsx index b9d0aa993..e4ae8acbe 100644 --- a/frontend/src/features/tasks/components/selector/SkillSelectorPopover.tsx +++ b/frontend/src/features/tasks/components/selector/SkillSelectorPopover.tsx @@ -282,12 +282,11 @@ const SkillSelectorPopover = forwardRef
setOpen(!open)} disabled={!hasSkills || disabled} icon={} + label={t('common:skillSelector.skill_button_label')} title={t('common:skillSelector.skill_button_tooltip')} - className="border-border bg-base text-text-primary hover:bg-hover" /> {selectedCount > 0 && ( void + teams: Team[] + disabled: boolean + taskDetail?: TaskDetail | null + hideSettingsLink?: boolean + /** Current mode for filtering teams by bind_mode */ + currentMode?: TaskType +} + +export default function TeamSelectorButton({ + selectedTeam, + setSelectedTeam, + teams, + disabled, + hideSettingsLink = false, + currentMode = 'chat', +}: TeamSelectorButtonProps) { + const { t } = useTranslation() + const router = useRouter() + const [open, setOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const sharedBadgeStyle = getSharedBadgeStyle() + + // Filter teams by bind_mode based on current mode + const filteredTeamsByMode = React.useMemo(() => { + // First filter out teams with empty bind_mode array + const teamsWithValidBindMode = teams.filter(team => { + if (Array.isArray(team.bind_mode) && team.bind_mode.length === 0) return false + return true + }) + + return teamsWithValidBindMode.filter(team => { + // If bind_mode is not set (undefined/null), show in all modes + if (!team.bind_mode) return true + // Otherwise, only show if current mode is in bind_mode + return team.bind_mode.includes(currentMode) + }) + }, [teams, currentMode]) + + // Filter teams by search query + const filteredTeams = filteredTeamsByMode.filter(team => + team.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + + const handleSelectTeam = (team: Team) => { + setSelectedTeam(team) + setOpen(false) + setSearchQuery('') + } + + const handleOpenChange = (newOpen: boolean) => { + if (disabled) return + setOpen(newOpen) + if (!newOpen) { + setSearchQuery('') + } + } + + if (!selectedTeam || teams.length === 0) return null + + return ( + + + + + +
+ setOpen(!open)} + disabled={disabled} + icon={} + label={t('common:teamSelector.agent_label', '智能体')} + /> +
+
+
+ +

{t('common:teamSelector.select_agent_tooltip', '选择智能体')}

+
+
+ + +
+ {t('common:teams.select_team')} +
+ + {/* Search input */} +
+
+ + setSearchQuery(e.target.value)} + placeholder={t('common:teams.search_team')} + className="h-8 pl-7 text-sm" + /> +
+
+ + {/* Teams list */} +
+ {filteredTeams.length === 0 ? ( +
+ {searchQuery ? t('common:teams.no_match') : t('common:teams.no_match')} +
+ ) : ( + filteredTeams.map(team => { + const isSelected = selectedTeam?.id === team.id + const isSharedTeam = team.share_status === 2 && team.user?.user_name + const isGroupTeam = + team.namespace && team.namespace !== 'default' && team.namespace !== 'community' + + return ( +
handleSelectTeam(team)} + role="button" + tabIndex={0} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleSelectTeam(team) + } + }} + > +
+ {isSelected && } +
+
+
+ + {team.name} + + {isGroupTeam && ( + + {team.namespace} + + )} + {isSharedTeam && ( + + {t('common:teams.shared_by', { author: team.user?.user_name })} + + )} +
+
+
+ ) + }) + )} +
+ + {/* Footer with settings link */} + {!hideSettingsLink && ( +
{ + router.push(paths.settings.team.getHref()) + setOpen(false) + }} + role="button" + tabIndex={0} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + router.push(paths.settings.team.getHref()) + setOpen(false) + } + }} + > + + + {t('common:teams.manage')} + +
+ )} +
+
+
+ ) +} diff --git a/frontend/src/features/tasks/components/selector/UnifiedRepositorySelector.tsx b/frontend/src/features/tasks/components/selector/UnifiedRepositorySelector.tsx index d92783fd4..795c46a08 100644 --- a/frontend/src/features/tasks/components/selector/UnifiedRepositorySelector.tsx +++ b/frontend/src/features/tasks/components/selector/UnifiedRepositorySelector.tsx @@ -403,12 +403,12 @@ export default function UnifiedRepositorySelector({
{/* Sidebar content container - hidden when collapsed */} {!isCollapsed && ( diff --git a/frontend/src/features/tasks/components/sidebar/TaskSidebar.tsx b/frontend/src/features/tasks/components/sidebar/TaskSidebar.tsx index d7e628b02..a08bd9083 100644 --- a/frontend/src/features/tasks/components/sidebar/TaskSidebar.tsx +++ b/frontend/src/features/tasks/components/sidebar/TaskSidebar.tsx @@ -92,7 +92,7 @@ export default function TaskSidebar({ const scrollRef = useRef(null) // Use external state for search dialog (controlled by parent page) - const setIsSearchDialogOpen = onSearchDialogOpenChange ?? (() => {}) + const setIsSearchDialogOpen = onSearchDialogOpenChange ?? (() => { }) // Group chats collapse/expand state const [isGroupChatsExpanded, setIsGroupChatsExpanded] = useState(false) @@ -269,11 +269,11 @@ export default function TaskSidebar({ Weibo Logo - Wegent + Wegent
{onToggleCollapsed && ( @@ -307,7 +307,7 @@ export default function TaskSidebar({ - - -

- {shortcutDisplayText - ? t('common:tasks.search_hint_with_shortcut', { - shortcut: shortcutDisplayText, - }) - : t('common:tasks.search_placeholder_chat')} -

-
- -
- {totalUnreadCount > 0 && ( - - )} + + + + + + +

+ {shortcutDisplayText + ? t('common:tasks.search_hint_with_shortcut', { + shortcut: shortcutDisplayText, + }) + : t('common:tasks.search_placeholder_chat')} +

+
+
+
)} {isCollapsed && filteredGroupTasks.length > 0 && ( -
+
)}