Skip to content

Conversation

@muhammadganiev
Copy link
Collaborator

@muhammadganiev muhammadganiev commented Nov 26, 2025

Description

  • Sand box was simplified
  • Web site became closer in style with energent ai
  • main focus on energent ai (most of buttons direct to it)
  • still some AnyParser references there
  • fixed video player, which was collapsing
Screenshot 2025-11-26 at 17 26 22 Screenshot 2025-11-26 at 17 26 33 Screenshot 2025-11-26 at 17 26 45 Screenshot 2025-11-26 at 17 26 55 Screenshot 2025-11-26 at 17 27 05 Screenshot 2025-11-26 at 17 28 26 Screenshot 2025-11-26 at 17 28 46 Screenshot 2025-11-26 at 17 29 03 Screenshot 2025-11-26 at 17 29 24

@github-actions
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Accessibility

Dropdown relies on mouseenter/mouseleave and lacks keyboard interactions and ARIA roles; consider adding role="menu", aria-expanded, focus trapping, and Escape handling for accessibility and screen readers.

return (
  <div className={cn('relative', className)} ref={dropdownRef}>
    <button
      className={cn(
        'px-6 py-2 text-sm transition-colors relative z-10 flex items-center gap-1 whitespace-nowrap',
        'text-foreground hover:bg-accent',
        triggerClassName
      )}
      onClick={() => setIsOpen(!isOpen)}
      onMouseEnter={() => setIsOpen(true)}
    >
      <span className="flex-shrink-0">{label}</span>
    </button>

    <AnimatePresence>
      {isOpen && (
        <motion.div
          initial={{ opacity: 0, y: -10, scale: 0.95 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: -10, scale: 0.95 }}
          transition={{ duration: 0.2, ease: 'easeOut' }}
          className={cn(
            'absolute top-full left-0 mt-1 z-50 rounded-md border shadow-lg',
            'bg-popover text-popover-foreground border-border',
            columnCount && columnCount > 1 ? 'min-w-[800px] max-h-[80vh] overflow-y-auto' : 'min-w-[200px]',
            dropdownClassName
          )}
          onMouseLeave={() => setIsOpen(false)}
        >
          <div className={cn('py-1', columnCount && columnCount > 1 ? `grid gap-0 ${getGridColumnsClass()}` : '')}>
            {items.map((item, index) => {
              const Icon = item.icon;
              const isMultiColumn = columnCount && columnCount > 1;
              const columnIndex = isMultiColumn ? index % columnCount : 0;
              const isLastInColumn = isMultiColumn && columnIndex === columnCount - 1;

              return item.isExternal ? (
                <a
                  key={item.id}
                  href={item.href}
                  target="_blank"
                  rel="noopener noreferrer"
                  className={cn(
                    'block px-4 py-2 text-sm transition-colors hover:bg-accent text-accent-foreground',
                    isMultiColumn && !isLastInColumn ? 'border-r border-border' : ''
                  )}
                  onClick={() => handleItemClick(item)}
                >
                  <div className="flex items-start gap-3">
                    {Icon && (
                      <div className="flex-shrink-0 mt-0.5">
                        <Icon className={cn('w-4 h-4', 'text-accent-foreground')} />
                      </div>
                    )}
                    <div className="flex-1">
                      <div className="font-medium">{item.label}</div>
                      {item.description && (
                        <div className={cn('text-xs mt-1', 'text-accent-foreground/80')}>{item.description}</div>
                      )}
                    </div>
                  </div>
                </a>
              ) : (
                <Link
                  key={item.id}
                  href={item.href}
                  className={cn(
                    'block px-4 py-2 text-sm transition-colors hover:bg-accent text-accent-foreground',
                    isMultiColumn && !isLastInColumn ? 'border-r border-border' : ''
                  )}
                  onClick={() => handleItemClick(item)}
                >
                  <div className="flex items-start gap-3">
                    {Icon && (
                      <div className="flex-shrink-0 mt-0.5">
                        <Icon className={cn('w-4 h-4', 'text-accent-foreground')} />
                      </div>
                    )}
                    <div className="flex-1">
                      <div className="font-medium">{item.label}</div>
                      {item.description && (
                        <div className={cn('text-xs mt-1', 'text-accent-foreground/80')}>{item.description}</div>
                      )}
                    </div>
                  </div>
                </Link>
              );
            })}
          </div>
        </motion.div>
      )}
    </AnimatePresence>
  </div>
);
SSR/CSR Mismatch

Active tab detection and scroll-driven header animation depend on window and pathname; ensure effects run client-side only and avoid layout shift/hydration mismatches (e.g., initial AnimatePresence/motion states and isMobile/scrollState initialization).

useEffect(() => {
  setShowNavbar(true);
}, []);

// Handle scroll events to determine navbar state
const handleScroll = useCallback(() => {
  const scrollY = window.scrollY;
  const next = scrollY < 50 ? 'hero' : 'transforming';
  if (next !== scrollState) setScrollState(next);
}, [scrollState]);

useEffect(() => {
  window.addEventListener('scroll', handleScroll, { passive: true });
  return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);

const handleNavClick = useCallback(() => setIsMenuOpen(false), []);

const navItems: NavbarTabItem[] = useMemo(
  () => [
    {
      id: 'pricing',
      label: t.nav.pricing,
      dropdown: [
        {
          id: 'energent-ai',
          label: 'Energent AI',
          href: 'https://app.energent.ai',
          isExternal: true,
        },
        {
          id: 'anyparser',
          label: 'AnyParser',
          href: `/${locale}/anyparser`,
        },
      ],
    },
    {
      id: 'blog',
      label: t.nav.blog,
      href: `/${locale}/blog`,
    },
    {
      id: 'company',
      label: t.nav.company,
      href: `/${locale}/company/about-us`,
    },
    {
      id: 'docs',
      label: t.nav.docs,
      link: 'https://docs.cambioml.com/introduction',
    },
  ],
  [t, locale]
);

// Determine active tab based on current route
useEffect(() => {
  const found = navItems.find((item) => {
    if (item.href && pathname.startsWith(item.href)) return true;
    if (item.dropdown) {
      return item.dropdown.some((sub) => pathname.startsWith(sub.href));
    }
    return false;
  });
  if (found && found.id !== activeTab) {
    setActiveTab(found.id);
  } else if (!found) {
    setActiveTab('');
  }
}, [pathname, navItems, activeTab]);

const handleTabChange = useCallback((id: string) => setActiveTab(id), []);

useEffect(() => {
  if (!isMobile && isMenuOpen) {
    setIsMenuOpen(false);
  }
}, [isMobile, isMenuOpen]);

// Close mobile menu when route changes
useEffect(() => {
  setIsMenuOpen(false);
}, [pathname]);

return (
  <>
    <AnimatePresence>
      {(showNavbar || scrollState !== 'hero') && (
        <motion.header
          className={cn(`w-full z-50`, {
            'absolute top-0 left-0': scrollState === 'hero' && !isMobile,
            'fixed top-0 left-0 right-0 backdrop-blur-lg bg-background/70': scrollState !== 'hero' || isMobile,
            'bg-background/95': isMenuOpen,
          })}
          initial={scrollState === 'hero' ? { opacity: 1 } : { y: -100, opacity: 0 }}
          animate={scrollState === 'hero' ? { opacity: 1 } : { y: 0, opacity: 1 }}
          exit={scrollState === 'hero' ? { opacity: 1 } : { y: -100, opacity: 0 }}
          key={!isMobile && scrollState === 'hero' ? 'hero-navbar' : 'fixed-navbar'}
        >
State Handling

The modal now closes immediately after upload and toggles an isLoading flag that is never rendered; confirm UX is intended and ensure race conditions are avoided when multiple files trigger UPLOADING/ADD_FILES transitions.

const { filesToUpload, addFiles, setFilesToUpload, files } = usePlaygroundStore();
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
  if (uploadModal.uploadModalState === UploadModalState.UPLOADING) {
    setIsLoading(true);
    filesToUpload.forEach((file) => {
      posthog.capture('playground.upload.start', { route: '/playground', file_type: file.type, module: 'upload' });
      addFiles({ files: file });
    });

    setFilesToUpload([]);
    posthog.capture('playground.upload.success', {
      route: '/playground',
      module: 'upload',
    });
    uploadModal.setUploadModalState(UploadModalState.ADD_FILES);
    setIsLoading(false);
    uploadModal.onClose();
    toast.success(t.messages.success.fileUploaded);
  }
}, [uploadModal.uploadModalState, uploadModal, filesToUpload, addFiles, posthog, setFilesToUpload, t]);

@github-actions
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent duplicate upload processing

The effect depends on filesToUpload and will rerun while in UPLOADING state, causing
duplicate addFiles and analytics events. Capture the list once and guard with a ref
to ensure the upload sequence runs only once per state transition.

app/components/modals/UploadModal.tsx [26-45]

+import { useRef } from 'react';
+...
+const hasProcessedUpload = useRef(false);
+
 useEffect(() => {
   if (uploadModal.uploadModalState === UploadModalState.UPLOADING) {
+    if (hasProcessedUpload.current) return;
+    hasProcessedUpload.current = true;
+
     setIsLoading(true);
-    filesToUpload.forEach((file) => {
+    const batch = [...filesToUpload];
+    batch.forEach((file) => {
       posthog.capture('playground.upload.start', { route: '/playground', file_type: file.type, module: 'upload' });
       addFiles({ files: file });
     });
   } else if (uploadModal.uploadModalState === UploadModalState.FINISHED) {
     posthog.capture('playground.upload.finish', {
       route: '/playground',
       event_name: 'playground.upload.finish',
       module: 'upload',
     });
     uploadModal.setUploadModalState(UploadModalState.ADD_FILES);
     setIsLoading(false);
+    hasProcessedUpload.current = false;
     uploadModal.onClose();
     toast.success(t.messages.success.fileUploaded);
+  } else {
+    hasProcessedUpload.current = false;
   }
-}, [uploadModal.uploadModalState, uploadModal, filesToUpload, addFiles, posthog, setFilesToUpload, t]);
+}, [uploadModal.uploadModalState, addFiles, filesToUpload, posthog, uploadModal, t, setIsLoading]);
Suggestion importance[1-10]: 8

__

Why: The effect depends on filesToUpload and can re-run causing duplicate uploads; adding a ref guard is a correct, impactful fix to prevent repeated addFiles and analytics events during the UPLOADING state.

Medium
Use Next.js router for navigation

Triggering navigation with window.location.href or window.open bypasses Next.js
routing and can break scroll/state restoration. Use Next's router for internal
routes and only use window.open for external links. Guard against SSR by accessing
window conditionally.

app/components/navbar/energent/navbar-tabs.tsx [100-120]

-const handleTabClick = (id: string) => {
-  const item = items.find((item) => item.id === id);
+import { useRouter } from 'next/navigation';
+...
+export default function NavbarTabs({...}: NavbarTabsProps) {
+  const router = useRouter();
+  ...
+  const handleTabClick = (id: string) => {
+    const item = items.find((it) => it.id === id);
+    if (!item) return;
 
-  // Don't handle click for dropdown items
-  if (item?.dropdown) {
-    return;
-  }
+    // Don't handle click for dropdown items
+    if (item.dropdown) return;
 
-  if (item?.link) {
-    window.open(item.link, '_blank');
-  } else if (item?.href) {
-    window.location.href = item.href;
-  } else {
+    if (item?.link) {
+      // external link
+      if (typeof window !== 'undefined') {
+        window.open(item.link, '_blank', 'noopener,noreferrer');
+      }
+      return;
+    }
+
+    if (item?.href) {
+      // internal route
+      router.push(item.href);
+      return;
+    }
+
     if (activeTab === undefined) {
       setLocalActiveTab(id);
     }
-    if (onChange) {
-      onChange(id);
-    }
-  }
-};
+    onChange?.(id);
+  };
+  ...
+}
Suggestion importance[1-10]: 7

__

Why: Using Next.js router for internal navigation improves routing behavior and state handling; the proposed code accurately replaces direct window navigation while preserving external link logic and dropdown guard.

Medium
Guard uninitialized analytics client

Casting posthog to any masks type errors and may pass an uninitialized client. Guard
against undefined client initialization to avoid runtime errors in edge cases.
Return children if the client isn't ready.

app/providers.tsx [15-19]

 export function PHProvider({ children }: { children: React.ReactNode }) {
-  if (typeof window === 'undefined') {
+  if (typeof window === 'undefined' || !posthog) {
     return <>{children}</>;
   }
-  return <PostHogProvider client={posthog as any}>{children}</PostHogProvider>;
+  return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
 }
Suggestion importance[1-10]: 7

__

Why: Removing the any cast and guarding against an uninitialized posthog avoids potential runtime issues; it's accurate and beneficial given the PR's client-only guard, though not a critical bug fix.

Medium
Prevent viewport height clipping

Using h-[calc(100vh-100px)] can cause content clipping on small screens and mobile
browsers where 100vh excludes UI chrome. Replace with a min-height that accommodates
content and allows scrolling. This prevents inaccessible hidden content.

app/components/playground/PlaygroundPageContainer.tsx [30-35]

 <div className={cn('relative w-full overflow-hidden bg-background text-foreground pt-[100px]')}>
   {/* Main content */}
-  <div className="w-full h-[calc(100vh-100px)] flex flex-col border-y border-border">
+  <div className="w-full min-h-[calc(100vh-100px)] flex flex-col border-y border-border">
     <PlaygroundContainer />
     <UploadModal />
   </div>
 </div>
Suggestion importance[1-10]: 5

__

Why: Replacing a fixed viewport height with min-height can reduce clipping on some mobile browsers; it's a reasonable UX improvement with moderate impact and aligns with the shown code.

Low
General
Guard window usage for SSR

Accessing window during SSR can throw. Add a guard before adding listeners to ensure
code runs only in the browser. Also initialize scrollState from the actual current
scrollY once mounted.

app/components/navbar/Navbar.tsx [70-73]

 useEffect(() => {
+  if (typeof window === 'undefined') return;
+
+  // initialize once on mount
+  handleScroll();
+
   window.addEventListener('scroll', handleScroll, { passive: true });
-  return () => window.removeEventListener('scroll', handleScroll);
+  return () => {
+    window.removeEventListener('scroll', handleScroll);
+  };
 }, [handleScroll]);
Suggestion importance[1-10]: 6

__

Why: Adding a typeof window check and initializing from handleScroll() is a safe improvement for SSR robustness and correctness; it's minor but valid and aligned with the existing code.

Low
Improve toggle button accessibility

The button uses aria-expanded without an aria-controls relationship, which can
confuse assistive technologies. Also explicitly set aria-pressed to reflect toggle
state for better accessibility. Tie the control to a predictable id so the menu
panel can reference it.

app/components/navbar/energent/hamburger-menu.tsx [39-48]

 <button
   className={cn(
     sizeClasses[size],
     'flex flex-col items-center justify-center rounded-lg bg-secondary text-secondary-foreground hover:bg-secondary/80 relative',
     className
   )}
   onClick={handleClick}
   aria-label={isOpen ? 'Close menu' : 'Open menu'}
-  aria-expanded={isOpen}
+  aria-expanded={!!isOpen}
+  aria-pressed={!!isOpen}
+  aria-controls="primary-navigation"
 >
Suggestion importance[1-10]: 6

__

Why: Adding aria-controls and aria-pressed improves accessibility semantics for a toggle; change is correct and low-risk, but not critical. The existing_code matches the new hunk and the improved_code reflects the described changes.

Low
Add lastmod to sitemap entries

Include a tag inside each entry so search engines can optimize crawl scheduling.
Use the generation timestamp in ISO 8601 format.

public/sitemap.xml [3]

 <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
-<sitemap><loc>https://www.cambioml.com/sitemap-0.xml</loc></sitemap>
+<sitemap>
+  <loc>https://www.cambioml.com/sitemap-0.xml</loc>
+  <lastmod>2025-11-26T00:00:00Z</lastmod>
+</sitemap>
 </sitemapindex>
Suggestion importance[1-10]: 6

__

Why: Adding a element inside the entry is valid per the sitemap protocol and can improve crawl efficiency; however, it introduces a hardcoded timestamp and is not critical.

Low
Ensure trailing newline

Add a newline at the end of the file to avoid "No newline at end of file" issues
that can break certain parsers and CI linters. This ensures consistent handling
across tools and platforms.

public/sitemap.xml [2-4]

+<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+<sitemap><loc>https://www.cambioml.com/sitemap-0.xml</loc></sitemap>
+</sitemapindex>
 
-
Suggestion importance[1-10]: 3

__

Why: The snippet corresponds to lines 2-4 of the new hunk and the improved_code equals existing_code, so it's a minor style fix with limited impact.

Low

@muhammadganiev muhammadganiev changed the title Fix/energent UI for any parser Cambio Ml website redesign Nov 26, 2025
@muhammadganiev
Copy link
Collaborator Author

fixed the necessary ones

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants