Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/app/tools/[toolId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default async function ToolLayout({ params, children }: ToolLayoutProps)
</div>

{/* Tab Navigation */}
<ToolTabs toolId={toolId} />
<ToolTabs toolId={toolId} toolConfig={toolConfig} />

{/* Content */}
{children}
Expand Down
18 changes: 10 additions & 8 deletions apps/web/src/app/tools/__tests__/tool-pages.vtest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,16 @@ describe('Tool Pages Structure', () => {
});

describe('Main Tools Page', () => {
it('should have a main tools listing page', async () => {
try {
const pageModule = await import('../page');
expect(pageModule.default).toBeDefined();
expect(typeof pageModule.default).toBe('function');
} catch (error) {
throw new Error('Main tools page missing');
}
it('should have a main tools listing page', () => {
const fs = require('fs');
const path = require('path');

const filePath = path.join(__dirname, '../page.tsx');
expect(fs.existsSync(filePath)).toBe(true);

const content = fs.readFileSync(filePath, 'utf-8');
expect(content).toContain('export default');
expect(content).toContain('ToolsIndexPage');
});
});

Expand Down
211 changes: 111 additions & 100 deletions apps/web/src/app/tools/components/GenericToolTryPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useState, FormEvent, ReactNode } from 'react';
import { toolSchemas } from '@roast/ai';
import { toolSchemas, toolRegistry } from '@roast/ai';
import { ErrorDisplay, SubmitButton, TextAreaField } from './common';
import { useToolExecution } from '../hooks/useToolExecution';
import { AuthenticatedToolPage } from './AuthenticatedToolPage';
Expand Down Expand Up @@ -378,10 +378,14 @@ export function GenericToolTryPage<TInput extends Record<string, any>, TOutput>(
}
};

return (
<AuthenticatedToolPage>
// Check if tool requires authentication
const toolConfig = toolRegistry[toolId];
const requiresAuth = toolConfig?.requiresAuth !== false; // Default to true if not specified

const content = (
<>
<div className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-6 bg-white p-6 rounded-lg shadow-sm border">
<form onSubmit={handleSubmit} className="space-y-6 bg-white p-6 rounded-lg shadow-sm border">
{fields.map(renderField)}

{/* Multiple examples with labels */}
Expand Down Expand Up @@ -420,107 +424,114 @@ export function GenericToolTryPage<TInput extends Record<string, any>, TOutput>(
</div>
)}

<SubmitButton
isLoading={isLoading}
disabled={!isFormValid()}
text={submitButtonText}
loadingText={loadingText}
className={submitButtonClassName}
/>
</form>
<ErrorDisplay error={error} />
<SubmitButton
isLoading={isLoading}
disabled={!isFormValid()}
text={submitButtonText}
loadingText={loadingText}
className={submitButtonClassName}
/>
</form>

<ErrorDisplay error={error} />

{result && (
<div className="mt-8" data-testid="tool-result">
{/* View Toggle and Save Button */}
{(!hideViewToggle || onSaveResult) && (
<div className="mb-4 flex gap-2 justify-between items-center">
{!hideViewToggle && (
<div className="flex gap-2">
<button
onClick={() => setShowRawJSON(false)}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
!showRawJSON
? 'bg-indigo-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Visual View
</button>
{result && (
<div className="mt-8" data-testid="tool-result">
{/* View Toggle and Save Button */}
{(!hideViewToggle || onSaveResult) && (
<div className="mb-4 flex gap-2 justify-between items-center">
{!hideViewToggle && (
<div className="flex gap-2">
<button
onClick={() => setShowRawJSON(false)}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
!showRawJSON
? 'bg-indigo-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Visual View
</button>
<button
onClick={() => setShowRawJSON(true)}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
showRawJSON
? 'bg-indigo-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Raw JSON
</button>
</div>
)}

{/* Spacer when hideViewToggle but have save button */}
{hideViewToggle && onSaveResult && <div />}

{/* Save Button */}
{onSaveResult && (
<div className="flex gap-2 items-center">
{savedId ? (
<>
<span className="text-sm text-green-600">✓ Saved</span>
{getSavedResultUrl && (
<a
href={getSavedResultUrl(savedId)}
className="px-4 py-2 rounded-lg font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors"
>
View Saved
</a>
)}
</>
) : (
<button
onClick={() => setShowRawJSON(true)}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
showRawJSON
? 'bg-indigo-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
onClick={handleSave}
disabled={isSaving}
className="px-4 py-2 rounded-lg font-medium bg-green-600 text-white hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Raw JSON
{isSaving ? 'Saving...' : saveButtonText}
</button>
</div>
)}

{/* Spacer when hideViewToggle but have save button */}
{hideViewToggle && onSaveResult && <div />}

{/* Save Button */}
{onSaveResult && (
<div className="flex gap-2 items-center">
{savedId ? (
<>
<span className="text-sm text-green-600">✓ Saved</span>
{getSavedResultUrl && (
<a
href={getSavedResultUrl(savedId)}
className="px-4 py-2 rounded-lg font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors"
>
View Saved
</a>
)}
</>
) : (
<button
onClick={handleSave}
disabled={isSaving}
className="px-4 py-2 rounded-lg font-medium bg-green-600 text-white hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSaving ? 'Saving...' : saveButtonText}
</button>
)}
</div>
)}
</div>
)}
)}
</div>
)}
</div>
)}

{/* Result Display */}
{!hideViewToggle && showRawJSON ? (
<div className="rounded-lg border bg-white p-6 shadow-sm">
<h3 className="mb-4 text-lg font-semibold">Full JSON Response</h3>
<pre className="overflow-x-auto whitespace-pre-wrap break-words rounded bg-gray-50 p-4 font-mono text-sm">
{JSON.stringify(result, null, 2)}
</pre>
</div>
) : (
renderResult(result)
)}
</div>
)}
</div>
{/* Result Display */}
{!hideViewToggle && showRawJSON ? (
<div className="rounded-lg border bg-white p-6 shadow-sm">
<h3 className="mb-4 text-lg font-semibold">Full JSON Response</h3>
<pre className="overflow-x-auto whitespace-pre-wrap break-words rounded bg-gray-50 p-4 font-mono text-sm">
{JSON.stringify(result, null, 2)}
</pre>
</div>
) : (
renderResult(result)
)}
</div>
)}
</div>

{/* Prompt Preview Modal */}
<Dialog open={showPromptModal} onOpenChange={setShowPromptModal}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Prompt Preview</DialogTitle>
</DialogHeader>
<div className="mt-4">
<pre className="whitespace-pre-wrap break-words rounded bg-gray-50 p-4 font-mono text-sm border border-gray-200">
{promptContent}
</pre>
</div>
</DialogContent>
</Dialog>
</AuthenticatedToolPage>
{/* Prompt Preview Modal */}
<Dialog open={showPromptModal} onOpenChange={setShowPromptModal}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Prompt Preview</DialogTitle>
</DialogHeader>
<div className="mt-4">
<pre className="whitespace-pre-wrap break-words rounded bg-gray-50 p-4 font-mono text-sm border border-gray-200">
{promptContent}
</pre>
</div>
</DialogContent>
</Dialog>
</>
);

// Only wrap in AuthenticatedToolPage if tool requires auth
return requiresAuth ? (
<AuthenticatedToolPage toolName={toolConfig?.name}>
{content}
</AuthenticatedToolPage>
) : content;
}
50 changes: 39 additions & 11 deletions apps/web/src/app/tools/components/ToolTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,29 @@

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useSession } from 'next-auth/react';
import { LockClosedIcon } from '@heroicons/react/24/outline';
import type { ToolConfig } from '@roast/ai';

interface ToolTabsProps {
toolId: string;
toolConfig: ToolConfig;
}

export function ToolTabs({ toolId }: ToolTabsProps) {
export function ToolTabs({ toolId, toolConfig }: ToolTabsProps) {
const pathname = usePathname();
const { data: session, status } = useSession();
const isLoading = status === 'loading';
const isAuthenticated = !!session?.user;

const normalizedPath = pathname.replace(/\/+$/, ''); // Remove trailing slashes
const isDocsPage = normalizedPath === `/tools/${toolId}/docs` || normalizedPath === `/tools/${toolId}`;
const isTryPage = normalizedPath === `/tools/${toolId}/try`;

// Check if tool requires auth and user is not authenticated
const requiresAuth = toolConfig.requiresAuth !== false; // Default to true if not specified
const isLocked = requiresAuth && !isAuthenticated && !isLoading;
Comment on lines +14 to +26
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

LGTM! Correct authentication state management.

The authentication check properly handles the loading state by excluding it from isLocked, preventing premature display of the lock icon while the session is being verified. The default auth requirement logic is consistent with GenericToolTryPage.tsx.

Optional: Consider test bypass consistency

AuthenticatedToolPage includes test bypass logic for Playwright tests (checking __test_bypass_auth and localStorage). ToolTabs doesn't include this, which might cause the tab to show as locked during tests even when the page itself allows access. This is likely fine if tests navigate directly to the /try route, but consider verifying test coverage handles this correctly.


🏁 Script executed:

#!/bin/bash
# Check if there are E2E tests that interact with tool tabs for auth-required tools
rg -n "ToolTabs|Try It" --type=ts --type=tsx -g "*test*" -g "*spec*" -A 3 -B 3

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for test files referencing ToolTabs or auth-related tests
rg -n "ToolTabs" --type=ts -g "*test*" -g "*spec*" -A 3 -B 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if __test_bypass_auth logic exists in AuthenticatedToolPage for comparison
rg -n "__test_bypass_auth" --type=ts -A 5 -B 5

Length of output: 2979


🏁 Script executed:

#!/bin/bash
# Search for e2e or test directory structure to understand test organization
fd -t f -e test.ts -e spec.ts -e e2e.ts | head -20

Length of output: 401


🏁 Script executed:

#!/bin/bash
# View the full ToolTabs.tsx file to see if it has test bypass logic
cat apps/web/src/app/tools/components/ToolTabs.tsx

Length of output: 2883


🏁 Script executed:

#!/bin/bash
# Check the tools-auth.spec.ts test file to see if it tests ToolTabs or tab locking
cat apps/web/tests/playwright/tools-auth.spec.ts

Length of output: 13590


🏁 Script executed:

#!/bin/bash
# Search for any test bypass checks in ToolTabs across all files
rg -n "test_bypass" apps/web/src/app/tools/components/ToolTabs.tsx

Length of output: 0


ToolTabs is missing test bypass logic that exists in AuthenticatedToolPage—add for consistency.

The verification confirms the inconsistency: AuthenticatedToolPage (lines 37-42) checks for __test_bypass_auth and localStorage bypass flags, but ToolTabs (lines 14-26 in the reviewed code) does not. The current tests work despite this because they navigate directly to /tools/{toolId}/try, bypassing the tab UI entirely. However, the inconsistency should be resolved to prevent issues if the tab UI is accessed through navigation or if tests are refactored to validate tab locking behavior.

Suggested fix: Add the same test bypass check to ToolTabs.tsx that exists in AuthenticatedToolPage.tsx:

  • Check (window as any).__test_bypass_auth or localStorage.getItem('playwright-auth-bypass')
  • Set isLocked = false when bypass is active (matching AuthenticatedToolPage behavior)
🤖 Prompt for AI Agents
In apps/web/src/app/tools/components/ToolTabs.tsx around lines 14 to 26,
ToolTabs lacks the test bypass logic present in AuthenticatedToolPage which
causes inconsistent test behavior; add the same bypass check by reading (window
as any).__test_bypass_auth || localStorage.getItem('playwright-auth-bypass')
and, when that flag is truthy, force isLocked to false (i.e., treat the tool as
unlocked), ensuring you guard access to window for SSR if needed so the bypass
only runs in the browser.


return (
<div className="mb-6">
<div className="border-b border-gray-200">
Expand All @@ -27,16 +39,32 @@ export function ToolTabs({ toolId }: ToolTabsProps) {
>
Documentation
</Link>
<Link
href={`/tools/${toolId}/try`}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
isTryPage
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Try It
</Link>

{isLocked ? (
<div
className="py-2 px-1 border-b-2 border-transparent font-medium text-sm opacity-50 cursor-not-allowed flex items-center gap-1.5 group relative"
title="Sign in required to try this tool"
>
<LockClosedIcon className="h-4 w-4" />
<span>Try It</span>
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-10">
Sign in required to try this tool
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-gray-900"></div>
</div>
</div>
) : (
<Link
href={`/tools/${toolId}/try`}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
isTryPage
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Try It
</Link>
)}
</nav>
</div>
</div>
Expand Down
Loading