diff --git a/apps/web/src/app/tools/[toolId]/layout.tsx b/apps/web/src/app/tools/[toolId]/layout.tsx index 652051478..6f44b25ba 100644 --- a/apps/web/src/app/tools/[toolId]/layout.tsx +++ b/apps/web/src/app/tools/[toolId]/layout.tsx @@ -45,7 +45,7 @@ export default async function ToolLayout({ params, children }: ToolLayoutProps) {/* Tab Navigation */} - + {/* Content */} {children} diff --git a/apps/web/src/app/tools/__tests__/tool-pages.vtest.tsx b/apps/web/src/app/tools/__tests__/tool-pages.vtest.tsx index e657bfc94..be2df58a0 100644 --- a/apps/web/src/app/tools/__tests__/tool-pages.vtest.tsx +++ b/apps/web/src/app/tools/__tests__/tool-pages.vtest.tsx @@ -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'); }); }); diff --git a/apps/web/src/app/tools/components/GenericToolTryPage.tsx b/apps/web/src/app/tools/components/GenericToolTryPage.tsx index d002baeae..f669ddc8d 100644 --- a/apps/web/src/app/tools/components/GenericToolTryPage.tsx +++ b/apps/web/src/app/tools/components/GenericToolTryPage.tsx @@ -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'; @@ -378,10 +378,14 @@ export function GenericToolTryPage, TOutput>( } }; - return ( - + // Check if tool requires authentication + const toolConfig = toolRegistry[toolId]; + const requiresAuth = toolConfig?.requiresAuth !== false; // Default to true if not specified + + const content = ( + <>
-
+ {fields.map(renderField)} {/* Multiple examples with labels */} @@ -420,107 +424,114 @@ export function GenericToolTryPage, TOutput>(
)} - - - - + + + + - {result && ( -
- {/* View Toggle and Save Button */} - {(!hideViewToggle || onSaveResult) && ( -
- {!hideViewToggle && ( -
- + {result && ( +
+ {/* View Toggle and Save Button */} + {(!hideViewToggle || onSaveResult) && ( +
+ {!hideViewToggle && ( +
+ + +
+ )} + + {/* Spacer when hideViewToggle but have save button */} + {hideViewToggle && onSaveResult &&
} + + {/* Save Button */} + {onSaveResult && ( +
+ {savedId ? ( + <> + ✓ Saved + {getSavedResultUrl && ( + + View Saved + + )} + + ) : ( -
- )} - - {/* Spacer when hideViewToggle but have save button */} - {hideViewToggle && onSaveResult &&
} - - {/* Save Button */} - {onSaveResult && ( -
- {savedId ? ( - <> - ✓ Saved - {getSavedResultUrl && ( - - View Saved - - )} - - ) : ( - - )} -
- )} -
- )} + )} +
+ )} +
+ )} - {/* Result Display */} - {!hideViewToggle && showRawJSON ? ( -
-

Full JSON Response

-
-                    {JSON.stringify(result, null, 2)}
-                  
-
- ) : ( - renderResult(result) - )} -
- )} -
+ {/* Result Display */} + {!hideViewToggle && showRawJSON ? ( +
+

Full JSON Response

+
+                  {JSON.stringify(result, null, 2)}
+                
+
+ ) : ( + renderResult(result) + )} +
+ )} +
- {/* Prompt Preview Modal */} - - - - Prompt Preview - -
-
-                {promptContent}
-              
-
-
-
-
+ {/* Prompt Preview Modal */} + + + + Prompt Preview + +
+
+              {promptContent}
+            
+
+
+
+ ); + + // Only wrap in AuthenticatedToolPage if tool requires auth + return requiresAuth ? ( + + {content} + + ) : content; } \ No newline at end of file diff --git a/apps/web/src/app/tools/components/ToolTabs.tsx b/apps/web/src/app/tools/components/ToolTabs.tsx index 958615466..d35aae0ba 100644 --- a/apps/web/src/app/tools/components/ToolTabs.tsx +++ b/apps/web/src/app/tools/components/ToolTabs.tsx @@ -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; + return (
@@ -27,16 +39,32 @@ export function ToolTabs({ toolId }: ToolTabsProps) { > Documentation - - Try It - + + {isLocked ? ( +
+ + Try It + {/* Tooltip */} +
+ Sign in required to try this tool +
+
+
+ ) : ( + + Try It + + )}
diff --git a/apps/web/src/app/tools/page.tsx b/apps/web/src/app/tools/page.tsx index 2f10b6ef0..c80164ae6 100644 --- a/apps/web/src/app/tools/page.tsx +++ b/apps/web/src/app/tools/page.tsx @@ -2,10 +2,12 @@ * Tools Index Page * Lists all available experimental tools */ +'use client'; import Link from 'next/link'; import { allToolConfigs } from '@roast/ai'; -import { MagnifyingGlassIcon, CpuChipIcon, CheckCircleIcon, FunnelIcon } from '@heroicons/react/24/outline'; +import { MagnifyingGlassIcon, CpuChipIcon, CheckCircleIcon, FunnelIcon, LockClosedIcon } from '@heroicons/react/24/outline'; +import { useSession } from 'next-auth/react'; const categoryIcons = { extraction: FunnelIcon, @@ -22,6 +24,10 @@ const categoryColors = { }; export default function ToolsIndexPage() { + const { data: session, status } = useSession(); + const isLoading = status === 'loading'; + const isAuthenticated = !!session?.user; + const tools = allToolConfigs; const toolsByCategory = tools.reduce((acc, tool) => { if (!acc[tool.category]) { @@ -31,6 +37,10 @@ export default function ToolsIndexPage() { return acc; }, {} as Record); + // Count how many tools require auth + const authRequiredCount = tools.filter(tool => tool.requiresAuth !== false).length; + const showAuthBanner = !isAuthenticated && !isLoading && authRequiredCount > 0; + return (
@@ -41,6 +51,23 @@ export default function ToolsIndexPage() {

+ {/* Auth Banner for Unauthenticated Users */} + {showAuthBanner && ( +
+ +
+

+ Sign in required: Most AI-powered tools require authentication to use. + You can still review how each tool works, but tools marked with a icon require you to{' '} + + sign in + {' '} + before you can try them. +

+
+
+ )} +
{Object.entries(toolsByCategory).map(([category, categoryTools]) => { const Icon = categoryIcons[category as keyof typeof categoryIcons] || MagnifyingGlassIcon; @@ -57,14 +84,21 @@ export default function ToolsIndexPage() { {categoryTools.map(tool => { // Link to docs page by default const toolPath = `/tools/${tool.id}/docs`; + const requiresAuth = tool.requiresAuth !== false; // Default to true if not specified + const showLock = requiresAuth && !isAuthenticated && !isLoading; return ( -

{tool.name}

+ {showLock && ( +
+ +
+ )} +

{tool.name}

{tool.description}

); diff --git a/internal-packages/ai/src/tools/base/Tool.ts b/internal-packages/ai/src/tools/base/Tool.ts index df5b7a01d..67e6bbe98 100644 --- a/internal-packages/ai/src/tools/base/Tool.ts +++ b/internal-packages/ai/src/tools/base/Tool.ts @@ -12,6 +12,7 @@ export interface ToolConfig { costEstimate?: string; path?: string; // UI route path status?: 'stable' | 'experimental' | 'beta'; + requiresAuth?: boolean; // Whether the tool requires authentication (defaults to true for AI-powered tools) } export interface ToolContext { diff --git a/internal-packages/ai/src/tools/configs.ts b/internal-packages/ai/src/tools/configs.ts index ede583b23..502a6da14 100644 --- a/internal-packages/ai/src/tools/configs.ts +++ b/internal-packages/ai/src/tools/configs.ts @@ -20,6 +20,7 @@ export const mathValidatorLLMConfig: ToolConfig = { costEstimate: "~$0.02 per check (1 Claude call with longer analysis)", path: "/tools/math-validator-llm", status: "stable", + requiresAuth: true, }; export const mathValidatorMathJsConfig: ToolConfig = { @@ -33,6 +34,7 @@ export const mathValidatorMathJsConfig: ToolConfig = { "~$0.02-0.05 per statement (uses Claude with multiple tool calls)", path: "/tools/math-validator-mathjs", status: "beta", + requiresAuth: true, }; export const mathValidatorHybridConfig: ToolConfig = { @@ -44,6 +46,7 @@ export const mathValidatorHybridConfig: ToolConfig = { costEstimate: "~$0.01-0.03 per check (computational + optional LLM)", path: "/tools/math-validator-hybrid", status: "stable", + requiresAuth: true, }; export const factCheckerConfig: ToolConfig = { @@ -55,6 +58,7 @@ export const factCheckerConfig: ToolConfig = { costEstimate: "~$0.01-0.02 per claim", path: "/tools/fact-checker", status: "stable", + requiresAuth: true, }; export const binaryForecasterConfig: ToolConfig = { @@ -67,6 +71,7 @@ export const binaryForecasterConfig: ToolConfig = { costEstimate: "~$0.05 per forecast (6 Claude calls)", path: "/tools/binary-forecaster", status: "experimental", + requiresAuth: true, }; export const fuzzyTextSearcherConfig: ToolConfig = { @@ -79,6 +84,7 @@ export const fuzzyTextSearcherConfig: ToolConfig = { costEstimate: "Free (or minimal LLM cost if fallback is used)", path: "/tools/smart-text-searcher", status: "stable", + requiresAuth: true, }; export const documentChunkerConfig: ToolConfig = { @@ -91,6 +97,7 @@ export const documentChunkerConfig: ToolConfig = { costEstimate: "$0 (no LLM calls)", path: "/tools/document-chunker", status: "stable", + requiresAuth: false, }; export const binaryForecastingClaimsExtractorConfig: ToolConfig = { @@ -103,6 +110,7 @@ export const binaryForecastingClaimsExtractorConfig: ToolConfig = { costEstimate: "~$0.01-0.03 per analysis (uses Claude Sonnet)", path: "/tools/binary-forecasting-claims-extractor", status: "beta", + requiresAuth: true, }; export const factualClaimsExtractorConfig: ToolConfig = { @@ -114,6 +122,7 @@ export const factualClaimsExtractorConfig: ToolConfig = { costEstimate: "~$0.01-0.03 per analysis (depends on text length)", path: "/tools/factual-claims-extractor", status: "stable", + requiresAuth: true, }; export const spellingGrammarCheckerConfig: ToolConfig = { @@ -126,6 +135,7 @@ export const spellingGrammarCheckerConfig: ToolConfig = { costEstimate: "~$0.01-0.02 per check", path: "/tools/spelling-grammar-checker", status: "stable", + requiresAuth: true, }; export const mathExpressionsExtractorConfig: ToolConfig = { @@ -138,6 +148,7 @@ export const mathExpressionsExtractorConfig: ToolConfig = { costEstimate: "~$0.02 per extraction (1 Claude call)", path: "/tools/math-expressions-extractor", status: "beta", + requiresAuth: true, }; export const languageConventionDetectorConfig: ToolConfig = { @@ -146,9 +157,10 @@ export const languageConventionDetectorConfig: ToolConfig = { description: "Detect whether text uses US or UK English conventions", version: "1.0.0", category: "checker", - costEstimate: "~$0.00 (no LLM calls)", + costEstimate: "~$0.01-0.02 per check (2 Claude calls)", path: "/tools/language-convention-detector", status: "stable", + requiresAuth: true, }; export const perplexityResearcherConfig: ToolConfig = { @@ -161,6 +173,7 @@ export const perplexityResearcherConfig: ToolConfig = { costEstimate: "~$0.001-0.005 per query (via OpenRouter)", path: "/tools/perplexity-researcher", status: "stable", + requiresAuth: true, }; export const linkValidatorConfig: ToolConfig = { @@ -173,6 +186,7 @@ export const linkValidatorConfig: ToolConfig = { costEstimate: "Free (no LLM usage)", path: "/tools/link-validator", status: "stable", + requiresAuth: false, }; export const claimEvaluatorConfig: ToolConfig = { @@ -185,6 +199,7 @@ export const claimEvaluatorConfig: ToolConfig = { costEstimate: "~$0.01-0.05 per claim (4+ model calls via OpenRouter)", path: "/tools/claim-evaluator", status: "experimental", + requiresAuth: true, }; export const fallacyExtractorConfig: ToolConfig = { @@ -197,6 +212,7 @@ export const fallacyExtractorConfig: ToolConfig = { costEstimate: "~$0.01-0.03 per analysis (uses Claude Sonnet)", path: "/tools/fallacy-extractor", status: "beta", + requiresAuth: true, }; export const fallacyReviewConfig: ToolConfig = { @@ -208,6 +224,7 @@ export const fallacyReviewConfig: ToolConfig = { category: "utility", path: "/tools/fallacy-review", status: "beta", + requiresAuth: true, }; // ============================================================================ diff --git a/internal-packages/ai/src/tools/generated-schemas.ts b/internal-packages/ai/src/tools/generated-schemas.ts index f8c77aada..0617d6702 100644 --- a/internal-packages/ai/src/tools/generated-schemas.ts +++ b/internal-packages/ai/src/tools/generated-schemas.ts @@ -3,7 +3,7 @@ * Generated by scripts/generate-tool-schemas.ts * DO NOT EDIT MANUALLY * - * Schema Hash: 03fd6c8c36ea8ad38a61ccf1fc2e8ccca8017fecbcacc7106fea3f470eebbe59 + * Schema Hash: 84c169ea35be216e43ccb41e1797f89164560bf9ad8cc72a382485461d2533c5 */ export const toolSchemas = { @@ -16,7 +16,8 @@ export const toolSchemas = { "category": "checker", "costEstimate": "~$0.01-0.02 per check", "path": "/tools/spelling-grammar-checker", - "status": "stable" + "status": "stable", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -178,7 +179,8 @@ export const toolSchemas = { "category": "extraction", "costEstimate": "~$0.01-0.03 per analysis (depends on text length)", "path": "/tools/factual-claims-extractor", - "status": "stable" + "status": "stable", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -335,7 +337,8 @@ export const toolSchemas = { "category": "checker", "costEstimate": "~$0.01-0.02 per claim", "path": "/tools/fact-checker", - "status": "stable" + "status": "stable", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -458,6 +461,7 @@ export const toolSchemas = { "costEstimate": "~$0.02-0.05 per statement (uses Claude with multiple tool calls)", "path": "/tools/math-validator-mathjs", "status": "beta", + "requiresAuth": true, "examples": [ "2 + 2 = 4", "The binomial coefficient \"10 choose 3\" equals 120", @@ -669,7 +673,8 @@ export const toolSchemas = { "category": "checker", "costEstimate": "~$0.02 per check (1 Claude call with longer analysis)", "path": "/tools/math-validator-llm", - "status": "stable" + "status": "stable", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -780,7 +785,8 @@ export const toolSchemas = { "category": "checker", "costEstimate": "~$0.01-0.03 per check (computational + optional LLM)", "path": "/tools/math-validator-hybrid", - "status": "stable" + "status": "stable", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -977,7 +983,8 @@ export const toolSchemas = { "category": "extraction", "costEstimate": "~$0.02 per extraction (1 Claude call)", "path": "/tools/math-expressions-extractor", - "status": "beta" + "status": "beta", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -1114,7 +1121,8 @@ export const toolSchemas = { "category": "extraction", "costEstimate": "~$0.01-0.03 per analysis (uses Claude Sonnet)", "path": "/tools/binary-forecasting-claims-extractor", - "status": "beta" + "status": "beta", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -1229,7 +1237,8 @@ export const toolSchemas = { "category": "utility", "costEstimate": "$0 (no LLM calls)", "path": "/tools/document-chunker", - "status": "stable" + "status": "stable", + "requiresAuth": false }, "inputSchema": { "type": "object", @@ -1391,7 +1400,8 @@ export const toolSchemas = { "category": "utility", "costEstimate": "Free (or minimal LLM cost if fallback is used)", "path": "/tools/smart-text-searcher", - "status": "stable" + "status": "stable", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -1500,9 +1510,10 @@ export const toolSchemas = { "description": "Detect whether text uses US or UK English conventions", "version": "1.0.0", "category": "checker", - "costEstimate": "~$0.00 (no LLM calls)", + "costEstimate": "~$0.01-0.02 per check (2 Claude calls)", "path": "/tools/language-convention-detector", - "status": "stable" + "status": "stable", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -1622,7 +1633,8 @@ export const toolSchemas = { "category": "research", "costEstimate": "~$0.05 per forecast (6 Claude calls)", "path": "/tools/binary-forecaster", - "status": "experimental" + "status": "experimental", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -1767,7 +1779,8 @@ export const toolSchemas = { "category": "checker", "costEstimate": "Free (no LLM usage)", "path": "/tools/link-validator", - "status": "stable" + "status": "stable", + "requiresAuth": false }, "inputSchema": { "type": "object", @@ -1938,7 +1951,8 @@ export const toolSchemas = { "category": "research", "costEstimate": "~$0.001-0.005 per query (via OpenRouter)", "path": "/tools/perplexity-researcher", - "status": "stable" + "status": "stable", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -2041,7 +2055,8 @@ export const toolSchemas = { "category": "research", "costEstimate": "~$0.01-0.05 per claim (4+ model calls via OpenRouter)", "path": "/tools/claim-evaluator", - "status": "experimental" + "status": "experimental", + "requiresAuth": true }, "inputSchema": { "type": "object", @@ -2370,7 +2385,8 @@ export const toolSchemas = { "category": "extraction", "costEstimate": "~$0.01-0.03 per analysis (uses Claude Sonnet)", "path": "/tools/fallacy-extractor", - "status": "beta" + "status": "beta", + "requiresAuth": true }, "inputSchema": { "type": "object",