Skip to content
Merged
111 changes: 81 additions & 30 deletions frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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<boolean> {
try {
Expand All @@ -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
}
}
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 20 additions & 16 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Expected result:
# - tailwind.config.js shows withOpacity() returning rgb(var(--token) / <alpha-value>)
# - globals.css shows --color-bg-hover already contains "/ 0.06" and "/ 0.1"
rg -n -C2 'withOpacity|--color-bg-hover' frontend/tailwind.config.js frontend/src/app/globals.css

Repository: wecode-ai/Wegent

Length of output: 5516


🏁 Script executed:

# Search for usages of bg-hover utility and opacity modifiers
rg -r 'bg-hover|accent-\[|accent/|accent-DEFAULT' frontend/src --type tsx --type ts --type jsx --type html -B2 -A2

Repository: wecode-ai/Wegent

Length of output: 87


🏁 Script executed:

# Check all CSS custom properties in globals.css to see if others have opacity values
rg '^\s*--.*:.*\/\s*[\d.]+;' frontend/src/app/globals.css -n

Repository: wecode-ai/Wegent

Length of output: 160


🏁 Script executed:

# Look for other color variable definitions that might have the same pattern
rg '^\s*--color-.*:' frontend/src/app/globals.css -n | head -30

Repository: wecode-ai/Wegent

Length of output: 1174


🏁 Script executed:

# Search for usages of bg-hover, accent colors, and --color-bg-hover in JSX/TSX
fd '\.(js|jsx|ts|tsx)$' frontend/src --exec rg -l 'bg-hover|accent|--color-bg-hover' {} \;

Repository: wecode-ai/Wegent

Length of output: 5042


🏁 Script executed:

# Get actual usage patterns
fd '\.(js|jsx|ts|tsx)$' frontend/src --exec rg 'bg-hover|accent-' {} + | head -20

Repository: wecode-ai/Wegent

Length of output: 31226


🏁 Script executed:

# Check if there are any Tailwind opacity modifiers used with these colors
rg '\b(bg-hover|accent)/' frontend/src -A2 -B2

Repository: wecode-ai/Wegent

Length of output: 725


Remove opacity values from --color-bg-hover CSS custom properties.

Tailwind's withOpacity() helper at frontend/tailwind.config.js:53 wraps --color-bg-hover as rgb(var(--color-bg-hover) / <alpha-value>). Defining the token as 93 94 201 / 0.06 creates invalid CSS like rgb(93 94 201 / 0.06 / 1) when utilities like bg-hover or opacity modifiers (bg-hover/50, bg-accent/50) are applied. Store only the raw RGB triplet (93 94 201 and 118 119 218) and let Tailwind apply opacity through standard utility modifiers.

Also applies to: 49-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/globals.css` at line 17, The CSS custom property
--color-bg-hover currently includes an opacity component ("93 94 201 / 0.06")
which breaks Tailwind's withOpacity() wrapper; remove the "/ 0.06" so the value
is just the raw RGB triplet ("93 94 201"). Do the same for any other tokens
using the "/ <alpha>" pattern (e.g., --color-accent if present as "118 119 218 /
0.06") so all color custom properties store only the RGB triplet and allow
Tailwind's withOpacity()/utility opacity modifiers to supply alpha values.

--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;
Expand All @@ -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;
}

Expand Down
47 changes: 47 additions & 0 deletions frontend/src/components/icons/AgentIcon.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g clipPath="url(#clip0_1277_3534)">
<path
d="M8.0311 8.04319C6.34454 8.04319 4.9705 6.67084 4.9705 4.98259C4.9705 3.29604 6.34285 1.922 8.0311 1.922C9.71935 1.922 11.0917 3.29434 11.0917 4.98259C11.09 6.67084 9.71765 8.04319 8.0311 8.04319ZM8.0311 3.25018C7.07487 3.25018 6.29869 4.02807 6.29869 4.98259C6.29869 5.93882 7.07657 6.715 8.0311 6.715C8.98732 6.715 9.76351 5.93712 9.76351 4.98259C9.76351 4.02807 8.98562 3.25018 8.0311 3.25018ZM11.1189 14.0183H4.94163C4.40153 14.0183 3.8886 13.7839 3.53532 13.3763C3.18204 12.9687 3.02239 12.4286 3.09882 11.8936L3.35189 10.1068C3.48097 9.19473 4.27414 8.50686 5.1964 8.50686H10.8658C11.7864 8.50686 12.5795 9.19473 12.7103 10.1068L12.9634 11.8936C13.0398 12.4286 12.8802 12.9687 12.5269 13.3763C12.1719 13.7839 11.659 14.0183 11.1189 14.0183ZM4.41172 12.0804C4.38114 12.291 4.47796 12.4371 4.5374 12.5067C4.59685 12.5763 4.72933 12.6918 4.94163 12.6918H11.1189C11.3312 12.6918 11.4637 12.5763 11.5231 12.5067C11.5825 12.4371 11.6794 12.291 11.6488 12.0804L11.3957 10.2936C11.3583 10.0321 11.1308 9.83334 10.8658 9.83334H5.1947C4.92974 9.83334 4.70215 10.0304 4.66478 10.2936L4.41172 12.0804ZM12.3638 8.08904C12.0734 8.08904 11.805 7.89542 11.7252 7.60159C11.6284 7.24831 11.8356 6.88315 12.1889 6.78634C12.7426 6.63348 13.1281 6.12564 13.1281 5.54987C13.1281 4.94353 12.6984 4.41531 12.104 4.29472C11.7456 4.22169 11.5129 3.87181 11.5859 3.51174C11.659 3.15337 12.0089 2.92068 12.3689 2.99371C13.5765 3.23999 14.4546 4.3151 14.4546 5.54987C14.4546 6.7201 13.6665 7.75445 12.5405 8.06527C12.481 8.08225 12.4216 8.08904 12.3638 8.08904ZM14.3527 13.0077H13.923C13.5561 13.0077 13.2589 12.7105 13.2589 12.3436C13.2589 11.9768 13.5561 11.6796 13.923 11.6796H14.3527C14.475 11.6796 14.5514 11.6133 14.5871 11.5726C14.6211 11.5335 14.6771 11.4486 14.6601 11.3263L14.4546 9.87581C14.4325 9.72464 14.3018 9.60915 14.1472 9.60915H13.889C13.5222 9.60915 13.2249 9.31192 13.2249 8.94506C13.2249 8.5782 13.5222 8.28097 13.889 8.28097H14.1472C14.9574 8.28097 15.6537 8.88561 15.7675 9.68728L15.973 11.1377C16.0393 11.6065 15.9 12.0821 15.5892 12.4405C15.2767 12.8022 14.8266 13.0077 14.3527 13.0077Z"
fill="currentColor"
/>
<path
d="M3.61519 8.08902C3.55744 8.08902 3.498 8.08053 3.43855 8.06525C2.31079 7.75443 1.52441 6.72008 1.52441 5.54985C1.52441 4.31508 2.4008 3.23997 3.6101 2.99369C3.97017 2.92066 4.32004 3.15335 4.39308 3.51172C4.46611 3.87009 4.23512 4.22167 3.87505 4.2947C3.2823 4.41529 2.85089 4.94351 2.85089 5.54985C2.85089 6.12562 3.23814 6.63346 3.79013 6.78632C4.14341 6.88313 4.35062 7.24829 4.25381 7.60157C4.17398 7.8954 3.90732 8.08902 3.61519 8.08902ZM2.05602 13.0077H1.62631C1.15245 13.0077 0.700663 12.8022 0.389848 12.4438C0.079033 12.0855 -0.0619378 11.6099 0.00599987 11.1411L0.211512 9.69235C0.325307 8.89069 1.02167 8.28604 1.83183 8.28604H2.09169C2.45855 8.28604 2.75578 8.58327 2.75578 8.95013C2.75578 9.317 2.45855 9.61423 2.09169 9.61423H1.83183C1.67897 9.61423 1.54649 9.72802 1.52611 9.88088L1.32059 11.3297C1.30361 11.4519 1.35966 11.5369 1.39363 11.5759C1.4276 11.615 1.50403 11.6829 1.62801 11.6829H2.05602C2.42288 11.6829 2.72011 11.9802 2.72011 12.347C2.72011 12.7139 2.42288 13.0077 2.05602 13.0077Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_1277_3534">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
)
}

export default AgentIcon
41 changes: 41 additions & 0 deletions frontend/src/components/icons/ModelIcon.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M8 1.00092C8.01781 1.00092 8.03533 1.00573 8.05078 1.01459L14.0234 4.46381H14.0244C14.0397 4.47276 14.0526 4.48554 14.0615 4.50092C14.0704 4.51633 14.0751 4.5339 14.0752 4.5517V11.4482C14.0752 11.466 14.0704 11.4835 14.0615 11.499C14.0527 11.5143 14.0397 11.5271 14.0244 11.5361H14.0234L8.05078 14.9853C8.0353 14.9942 8.01785 14.999 8 14.999C7.98215 14.999 7.9647 14.9942 7.94922 14.9853L1.97656 11.5361H1.97559C1.96031 11.5271 1.94733 11.5143 1.93848 11.499C1.92962 11.4835 1.92482 11.466 1.9248 11.4482V4.5517L1.93848 4.49994C1.94737 4.48459 1.96026 4.47176 1.97559 4.46283L7.94922 1.01459C7.96467 1.00574 7.98219 1.00092 8 1.00092Z"
stroke="currentColor"
strokeWidth="1.35"
/>
<path
d="M10.9014 6.84407C11.2546 6.64017 11.3749 6.18881 11.1717 5.8363C11.1232 5.75242 11.0587 5.6789 10.9818 5.61995C10.9049 5.561 10.8172 5.51778 10.7236 5.49274C10.63 5.46771 10.5324 5.46136 10.4364 5.47406C10.3404 5.48675 10.2478 5.51824 10.1639 5.56673L7.99906 6.81642L5.8356 5.56673C5.75168 5.51728 5.6588 5.48493 5.56232 5.47157C5.46583 5.45821 5.36766 5.46409 5.27346 5.48888C5.17926 5.51367 5.09091 5.55687 5.0135 5.61599C4.93609 5.67512 4.87115 5.74899 4.82245 5.83334C4.77374 5.91769 4.74223 6.01086 4.72973 6.10746C4.71723 6.20406 4.72399 6.30218 4.74962 6.39615C4.77525 6.49013 4.81924 6.57809 4.87905 6.65497C4.93886 6.73185 5.0133 6.79612 5.09809 6.84407L7.26224 8.09376V8.11933V10.7735C7.26224 10.9691 7.33994 11.1567 7.47825 11.295C7.61656 11.4334 7.80415 11.5111 7.99975 11.5111C8.19535 11.5111 8.38294 11.4334 8.52125 11.295C8.65956 11.1567 8.73726 10.9691 8.73726 10.7735V8.11864L8.73657 8.09376L10.9014 6.84407Z"
fill="currentColor"
/>
</svg>
)
}

export default ModelIcon
22 changes: 18 additions & 4 deletions frontend/src/components/ui/action-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -79,24 +90,27 @@ export function ActionButton({
className={`relative ${baseStyles} flex items-center justify-center border border-border bg-base ${className}`}
>
{icon}
{label && <span className="text-sm text-text-primary">{label}</span>}
</div>
)
}

// 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 (
<Button
variant={buttonVariant}
size="icon"
size={hasLabel ? 'default' : 'icon'}
onClick={onClick}
disabled={disabled}
title={title}
className={`${baseStyles} ${defaultClassName} ${className}`}
>
{icon}
{label && <span className="text-sm text-text-primary">{label}</span>}
</Button>
)
}
Expand Down
Loading