diff --git a/frontend/src/apis/github.ts b/frontend/src/apis/github.ts index 39884e02..345ad0cf 100644 --- a/frontend/src/apis/github.ts +++ b/frontend/src/apis/github.ts @@ -31,9 +31,20 @@ export const githubApis = { return await apiClient.get('/git/repositories') }, - async searchRepositories(query: string): Promise { - // Add timeout=30 parameter to be compatible with backend interface - return await apiClient.get(`/git/repositories/search?q=${encodeURIComponent(query)}&timeout=30`); + // Unified search API: supports optional precise search via fullmatch and configurable timeout + async searchRepositories( + query: string, + opts?: { fullmatch?: boolean; timeout?: number } + ): Promise { + const timeout = opts?.timeout ?? 30 + const params = new URLSearchParams({ + q: query, + timeout: String(timeout), + }) + if (opts?.fullmatch) { + params.append('fullmatch', '1') + } + return await apiClient.get(`/git/repositories/search?${params.toString()}`) }, async getBranches(repo: GitRepoInfo): Promise { diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 6e022b25..c22c3fee 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -9,8 +9,9 @@ import { Button } from 'antd' import { paths } from '@/config/paths' import { useTranslation } from '@/hooks/useTranslation' import LanguageSwitcher from '@/components/LanguageSwitcher' -import { ThemeToggle } from '@/features/theme/ThemeToggle' import { getToken } from '@/apis/user' +import { ThemeToggle } from '@/features/theme/ThemeToggle' +import { GithubStarButton } from '@/features/layout/GithubStarButton' export default function Home() { const router = useRouter() @@ -29,6 +30,7 @@ export default function Home() {
{/* Language Switcher */}
+
diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index baa4c554..859fb0d7 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -16,8 +16,7 @@ import BotList from '@/features/settings/components/BotList' import TeamList from '@/features/settings/components/TeamList' import { UserProvider, useUser } from '@/features/common/UserContext' import { useTranslation } from '@/hooks/useTranslation' -import { ThemeToggle } from '@/features/theme/ThemeToggle' -import { DocsButton } from '@/features/layout/DocsButton' +import { GithubStarButton } from '@/features/layout/GithubStarButton' function DashboardContent() { const router = useRouter() @@ -78,9 +77,7 @@ function DashboardContent() { activePage="dashboard" showLogo={true} > - {/* User Avatar Menu */} - - + diff --git a/frontend/src/app/tasks/page.tsx b/frontend/src/app/tasks/page.tsx index c94cf77d..31954287 100644 --- a/frontend/src/app/tasks/page.tsx +++ b/frontend/src/app/tasks/page.tsx @@ -18,8 +18,7 @@ import TeamShareHandler from '@/features/tasks/components/TeamShareHandler' import OidcTokenHandler from '@/features/login/components/OidcTokenHandler' import '@/app/tasks/tasks.css' import '@/features/common/scrollbar.css' -import { ThemeToggle } from '@/features/theme/ThemeToggle' -import { DocsButton } from '@/features/layout/DocsButton' +import { GithubStarButton } from '@/features/layout/GithubStarButton' import { Team } from '@/types/api' export default function TasksPage() { // Team state from service @@ -69,8 +68,7 @@ export default function TasksPage() { showLogo={false} onMobileSidebarToggle={() => setIsMobileSidebarOpen(true)} > - - + {/* Chat area */} diff --git a/frontend/src/features/common/UserContext.tsx b/frontend/src/features/common/UserContext.tsx index 59efd330..d223af94 100644 --- a/frontend/src/features/common/UserContext.tsx +++ b/frontend/src/features/common/UserContext.tsx @@ -32,11 +32,9 @@ export const UserProvider = ({ children }: { children: ReactNode }) => { const fetchUser = async () => { setIsLoading(true) - console.log('UserContext: Starting to fetch user information') try { const isAuth = userApis.isAuthenticated() - console.log('UserContext: Authentication status check:', isAuth) if (!isAuth) { console.log('UserContext: User not authenticated, clearing user state and redirecting to login') diff --git a/frontend/src/features/layout/GithubStarButton.tsx b/frontend/src/features/layout/GithubStarButton.tsx new file mode 100644 index 00000000..2713ed4c --- /dev/null +++ b/frontend/src/features/layout/GithubStarButton.tsx @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2025 Weibo, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { useEffect, useState } from 'react' + +const REPO_API = 'https://api.github.com/repos/wecode-ai/Wegent' +const REPO_URL = 'https://github.com/wecode-ai/Wegent' + +export function GithubStarButton({ className = '' }: { className?: string }) { + const [stars, setStars] = useState(null) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + let isMounted = true + + const fetchStars = async () => { + try { + const response = await fetch(REPO_API, { + headers: { + Accept: 'application/vnd.github+json', + }, + }) + + if (!response.ok) { + throw new Error(`GitHub API responded with ${response.status}`) + } + + const data = await response.json() + + if (isMounted && typeof data?.stargazers_count === 'number') { + setStars(data.stargazers_count) + } + } catch (error) { + console.error('Failed to fetch GitHub stars', error) + if (isMounted) { + setStars(null) + } + } finally { + if (isMounted) { + setIsLoading(false) + } + } + } + + void fetchStars() + + return () => { + isMounted = false + } + }, []) + + const mergedClassName = ` + px-3 py-1.5 rounded-full border border-transparent + flex items-center gap-2 text-base font-semibold text-text-primary + hover:border-border transition-colors duration-200 + ${className} + `.trim() + + const handleClick = () => { + window.open(REPO_URL, '_blank', 'noopener,noreferrer') + } + + const formatStarCount = (value: number | null): string => { + if (value === null) { + return '—' + } + if (value < 1000) { + return "🌟"+value.toString() + } + const thousands = value / 1000 + const decimals = value >= 100000 ? 0 : 1 + const base = Number(thousands.toFixed(decimals)) + return `${base}k` + } + + const displayValue = isLoading ? '...' : formatStarCount(stars) + + return ( + + ) +} diff --git a/frontend/src/features/layout/UserMenu.tsx b/frontend/src/features/layout/UserMenu.tsx index 9122d0a7..f98a604f 100644 --- a/frontend/src/features/layout/UserMenu.tsx +++ b/frontend/src/features/layout/UserMenu.tsx @@ -9,6 +9,8 @@ import { Button } from 'antd' import { useUser } from '@/features/common/UserContext' import { useTranslation } from '@/hooks/useTranslation' +import { DocsButton } from '@/features/layout/DocsButton' +import { ThemeToggle } from '@/features/theme/ThemeToggle' type UserMenuProps = { className?: string @@ -22,13 +24,18 @@ export default function UserMenu({ className = '' }: UserMenuProps) { return (
- + {userDisplayName} +
+ + +
+
{({ active }) => (
-
- - - {/* Bottom Controls */} -
- - - {selectedRepo && ( - + {/* Messages Area: always mounted to keep scroll container stable */} +
+
+ +
+
+ + {/* Input Area: always mounted */} +
+ {/* Chat Input Card */} +
+ + {/* Team Selector and Send Button */} +
+
+ {teams.length > 0 && ( + )}
-
- - ) : ( -
- {/* Error Message */} - {/* Error prompt unified with antd message, no local rendering */} - {/* Chat Input */} -
- - {/* Team Selector and Send Button */} -
-
- {teams.length > 0 && ( - - )} -
-
- -
+
+ +
+
- {/* Bottom Controls */} -
- + + + {selectedRepo && ( + - - {selectedRepo && ( - - )} -
+ )}
- )} +
) } diff --git a/frontend/src/features/tasks/components/RepositorySelector.tsx b/frontend/src/features/tasks/components/RepositorySelector.tsx index 62b15e24..c1baac1f 100644 --- a/frontend/src/features/tasks/components/RepositorySelector.tsx +++ b/frontend/src/features/tasks/components/RepositorySelector.tsx @@ -35,7 +35,7 @@ export default function RepositorySelector({ const router = useRouter() const [repos, setRepos] = useState([]) const [loading, setLoading] = useState(false) - // Used antd message.error for unified error prompt, no need for local error state + // Used antd message.error for unified error prompt, no need for local error state const [error, setError] = useState(null) const [isModalOpen, setIsModalOpen] = useState(false) const searchTimeout = useRef(null) @@ -44,7 +44,7 @@ export default function RepositorySelector({ return user && user.git_info && user.git_info.length > 0 } - // Repository loading function, called when button is clicked + // Repository loading function, called when button is clicked const handleLoadRepos = () => { if (!hasGitInfo()) { return @@ -55,10 +55,6 @@ export default function RepositorySelector({ .then((data) => { setRepos(data) setError(null) - // Automatically select the first repository only on initial load - if (data.length > 0 && !selectedRepo) { - handleRepoChange(data[0]) - } }) .catch(() => { setError('Failed to load repositories') @@ -98,48 +94,69 @@ export default function RepositorySelector({ value: repo.git_repo_id, })) - - // Automatically load repositories on first mount (when git_info exists and repos is empty) + // Listen to selectedTaskDetail, auto-locate repository useEffect(() => { - if (hasGitInfo() && repos.length === 0) { - handleLoadRepos() + let canceled = false + + const tryLocateRepo = async () => { + if (selectedTaskDetail?.git_repo) { + // First, try to find in existing list + const repo = repos.find(r => r.git_repo === selectedTaskDetail.git_repo) + if (repo) { + handleRepoChange(repo) + return + } + // Fallback: precise search via fullmatch when not found locally + try { + setLoading(true) + const result = await githubApis.searchRepositories(selectedTaskDetail.git_repo, { fullmatch: true }) + if (canceled) return + if (result && result.length > 0) { + const matched = result.find(r => r.git_repo === selectedTaskDetail.git_repo) ?? result[0] + handleRepoChange(matched) + setError(null) + } else { + message.error('No repositories found') + } + } catch { + setError('Failed to search repositories') + message.error('Failed to search repositories') + } finally { + if (!canceled) setLoading(false) + } + } else { + handleRepoChange(null) + } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user]) - // Listen to selectedTaskDetail, auto-locate repository - useEffect(() => { - if (selectedTaskDetail?.git_repo) { - // Find the repository object in the list that matches git_repo - const repo = repos.find(r => r.git_repo === selectedTaskDetail.git_repo) - if (repo) { - handleRepoChange(repo) - } - } else { - handleRepoChange(null) + tryLocateRepo() + return () => { + canceled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedTaskDetail?.git_repo, repos]) - // Extract onOpenChange logic + // Extract onOpenChange logic + // load git repositories on first open const handleOpenChange = (visible: boolean) => { if (!hasGitInfo() && visible) { setIsModalOpen(true) } - // If repository not loaded and git_info exists, load repositories on first dropdown open - if (visible && hasGitInfo() && repos.length === 0 && !loading) { + // If repository not loaded and git_info exists, load repositories on first dropdown open + // fisrt click + if (visible && repos.length == 0 && hasGitInfo() && !loading) { handleLoadRepos() + return } } - // Extract onClick logic + // Extract onClick logic const handleModalClick = () => { setIsModalOpen(false) router.push(paths.settings.integrations.getHref()) } const { t } = useTranslation() - // Git icon is independent of Select, no longer needs renderLabel return (
@@ -157,7 +174,7 @@ export default function RepositorySelector({ popupMatchSelectWidth={false} styles={{ popup: { root: { maxWidth: 200 } } }} classNames={{ popup: { root: "repository-selector-dropdown custom-scrollbar" } }} - disabled={disabled || loading} + disabled={disabled} loading={loading} filterOption={false} onSearch={handleSearch} @@ -170,14 +187,15 @@ export default function RepositorySelector({
) : !loading ? (
- {'No repositories found'} + {repos.length === 0 ? 'Select Repository' : 'No repositories found'}
) : null } options={repoOptions} - // Disable dropdown selection and search (when no git_info) + // Disable dropdown selection and search (when no git_info) open={hasGitInfo() ? undefined : false} onOpenChange={handleOpenChange} + onClear={handleLoadRepos} /> { - setSelectedTask(null) if (typeof window !== 'undefined') { router.replace(paths.task.getHref()) }