diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 7ddf2abbcaa..1b0bd50c05b 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -99,8 +99,6 @@ function SignupFormContent({ const [showEmailValidationError, setShowEmailValidationError] = useState(false) const [formError, setFormError] = useState(null) const turnstileRef = useRef(null) - const captchaResolveRef = useRef<((token: string) => void) | null>(null) - const captchaRejectRef = useRef<((reason: Error) => void) | null>(null) const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), []) const redirectUrl = useMemo( () => searchParams.get('redirect') || searchParams.get('callbackUrl') || '', @@ -258,27 +256,14 @@ function SignupFormContent({ let token: string | undefined const widget = turnstileRef.current if (turnstileSiteKey && widget) { - let timeoutId: ReturnType | undefined try { widget.reset() - token = await Promise.race([ - new Promise((resolve, reject) => { - captchaResolveRef.current = resolve - captchaRejectRef.current = reject - widget.execute() - }), - new Promise((_, reject) => { - timeoutId = setTimeout(() => reject(new Error('Captcha timed out')), 15_000) - }), - ]) + widget.execute() + token = await widget.getResponsePromise() } catch { setFormError('Captcha verification failed. Please try again.') setIsLoading(false) return - } finally { - clearTimeout(timeoutId) - captchaResolveRef.current = null - captchaRejectRef.current = null } } @@ -535,10 +520,7 @@ function SignupFormContent({ captchaResolveRef.current?.(token)} - onError={() => captchaRejectRef.current?.(new Error('Captcha verification failed'))} - onExpire={() => captchaRejectRef.current?.(new Error('Captcha token expired'))} - options={{ execution: 'execute' }} + options={{ execution: 'execute', appearance: 'execute' }} /> )} diff --git a/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx index fd7557e37c7..c539d739daf 100644 --- a/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx +++ b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx @@ -18,6 +18,7 @@ import { formatPrice, formatTokenCount, formatUpdatedAt, + getEffectiveMaxOutputTokens, getModelBySlug, getPricingBounds, getProviderBySlug, @@ -198,7 +199,8 @@ export default async function ModelPage({

- {model.summary} {model.bestFor} + {model.summary} + {model.bestFor ? ` ${model.bestFor}` : ''}

@@ -229,13 +231,11 @@ export default async function ModelPage({ ? `${formatPrice(model.pricing.cachedInput)}/1M` : 'N/A' } - compact /> @@ -280,12 +280,12 @@ export default async function ModelPage({ label='Max output' value={ model.capabilities.maxOutputTokens - ? `${formatTokenCount(model.capabilities.maxOutputTokens)} tokens` - : 'Standard defaults' + ? `${formatTokenCount(getEffectiveMaxOutputTokens(model.capabilities))} tokens` + : 'Not published' } /> - + {model.bestFor ? : null}
diff --git a/apps/sim/app/(landing)/models/utils.test.ts b/apps/sim/app/(landing)/models/utils.test.ts new file mode 100644 index 00000000000..894c74500c9 --- /dev/null +++ b/apps/sim/app/(landing)/models/utils.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import { buildModelCapabilityFacts, getEffectiveMaxOutputTokens, getModelBySlug } from './utils' + +describe('model catalog capability facts', () => { + it.concurrent( + 'shows structured outputs support and published max output tokens for gpt-4o', + () => { + const model = getModelBySlug('openai', 'gpt-4o') + + expect(model).not.toBeNull() + expect(model).toBeDefined() + + const capabilityFacts = buildModelCapabilityFacts(model!) + const structuredOutputs = capabilityFacts.find((fact) => fact.label === 'Structured outputs') + const maxOutputTokens = capabilityFacts.find((fact) => fact.label === 'Max output tokens') + + expect(getEffectiveMaxOutputTokens(model!.capabilities)).toBe(16384) + expect(structuredOutputs?.value).toBe('Supported') + expect(maxOutputTokens?.value).toBe('16k') + } + ) + + it.concurrent('preserves native structured outputs labeling for claude models', () => { + const model = getModelBySlug('anthropic', 'claude-sonnet-4-6') + + expect(model).not.toBeNull() + expect(model).toBeDefined() + + const capabilityFacts = buildModelCapabilityFacts(model!) + const structuredOutputs = capabilityFacts.find((fact) => fact.label === 'Structured outputs') + + expect(structuredOutputs?.value).toBe('Supported (native)') + }) + + it.concurrent('does not invent a max output token limit when one is not published', () => { + expect(getEffectiveMaxOutputTokens({})).toBeNull() + }) + + it.concurrent('keeps best-for copy for clearly differentiated models only', () => { + const researchModel = getModelBySlug('google', 'deep-research-pro-preview-12-2025') + const generalModel = getModelBySlug('xai', 'grok-4-latest') + + expect(researchModel).not.toBeNull() + expect(generalModel).not.toBeNull() + + expect(researchModel?.bestFor).toContain('research workflows') + expect(generalModel?.bestFor).toBeUndefined() + }) +}) diff --git a/apps/sim/app/(landing)/models/utils.ts b/apps/sim/app/(landing)/models/utils.ts index cdf79f87b7c..8e649c95c6b 100644 --- a/apps/sim/app/(landing)/models/utils.ts +++ b/apps/sim/app/(landing)/models/utils.ts @@ -112,7 +112,7 @@ export interface CatalogModel { capabilities: ModelCapabilities capabilityTags: string[] summary: string - bestFor: string + bestFor?: string searchText: string } @@ -190,6 +190,14 @@ export function formatCapabilityBoolean( return value ? positive : negative } +function supportsCatalogStructuredOutputs(capabilities: ModelCapabilities): boolean { + return !capabilities.deepResearch +} + +export function getEffectiveMaxOutputTokens(capabilities: ModelCapabilities): number | null { + return capabilities.maxOutputTokens ?? null +} + function trimTrailingZeros(value: string): string { return value.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1') } @@ -326,7 +334,7 @@ function buildCapabilityTags(capabilities: ModelCapabilities): string[] { tags.push('Tool choice') } - if (capabilities.nativeStructuredOutputs) { + if (supportsCatalogStructuredOutputs(capabilities)) { tags.push('Structured outputs') } @@ -365,7 +373,7 @@ function buildBestForLine(model: { pricing: PricingInfo capabilities: ModelCapabilities contextWindow: number | null -}): string { +}): string | null { const { pricing, capabilities, contextWindow } = model if (capabilities.deepResearch) { @@ -376,10 +384,6 @@ function buildBestForLine(model: { return 'Best for reasoning-heavy tasks that need more deliberate model control.' } - if (pricing.input <= 0.2 && pricing.output <= 1.25) { - return 'Best for cost-sensitive automations, background tasks, and high-volume workloads.' - } - if (contextWindow && contextWindow >= 1000000) { return 'Best for long-context retrieval, large documents, and high-memory workflows.' } @@ -388,7 +392,11 @@ function buildBestForLine(model: { return 'Best for production workflows that need reliable typed outputs.' } - return 'Best for general-purpose AI workflows inside Sim.' + if (pricing.input <= 0.2 && pricing.output <= 1.25) { + return 'Best for cost-sensitive automations, background tasks, and high-volume workloads.' + } + + return null } function buildModelSummary( @@ -437,6 +445,11 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => { const shortId = stripProviderPrefix(provider.id, model.id) const mergedCapabilities = { ...provider.capabilities, ...model.capabilities } const capabilityTags = buildCapabilityTags(mergedCapabilities) + const bestFor = buildBestForLine({ + pricing: model.pricing, + capabilities: mergedCapabilities, + contextWindow: model.contextWindow ?? null, + }) const displayName = formatModelDisplayName(provider.id, model.id) const modelSlug = slugify(shortId) const href = `/models/${providerSlug}/${modelSlug}` @@ -461,11 +474,7 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => { model.contextWindow ?? null, capabilityTags ), - bestFor: buildBestForLine({ - pricing: model.pricing, - capabilities: mergedCapabilities, - contextWindow: model.contextWindow ?? null, - }), + ...(bestFor ? { bestFor } : {}), searchText: [ provider.name, providerDisplayName, @@ -683,6 +692,7 @@ export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel): export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[] { const { capabilities } = model + const supportsStructuredOutputs = supportsCatalogStructuredOutputs(capabilities) return [ { @@ -711,7 +721,11 @@ export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[] }, { label: 'Structured outputs', - value: formatCapabilityBoolean(capabilities.nativeStructuredOutputs), + value: supportsStructuredOutputs + ? capabilities.nativeStructuredOutputs + ? 'Supported (native)' + : 'Supported' + : 'Not supported', }, { label: 'Tool choice', @@ -732,8 +746,8 @@ export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[] { label: 'Max output tokens', value: capabilities.maxOutputTokens - ? formatTokenCount(capabilities.maxOutputTokens) - : 'Standard defaults', + ? formatTokenCount(getEffectiveMaxOutputTokens(capabilities)) + : 'Not published', }, ] } @@ -752,8 +766,8 @@ export function getProviderCapabilitySummary(provider: CatalogProvider): Capabil const reasoningCount = provider.models.filter( (model) => model.capabilities.reasoningEffort || model.capabilities.thinking ).length - const structuredCount = provider.models.filter( - (model) => model.capabilities.nativeStructuredOutputs + const structuredCount = provider.models.filter((model) => + supportsCatalogStructuredOutputs(model.capabilities) ).length const deepResearchCount = provider.models.filter( (model) => model.capabilities.deepResearch diff --git a/apps/sim/app/llms.txt/route.ts b/apps/sim/app/llms.txt/route.ts index 79c79d086ec..89fbc5a67f4 100644 --- a/apps/sim/app/llms.txt/route.ts +++ b/apps/sim/app/llms.txt/route.ts @@ -1,42 +1,44 @@ import { getBaseUrl } from '@/lib/core/utils/urls' -export async function GET() { +export function GET() { const baseUrl = getBaseUrl() - const llmsContent = `# Sim + const content = `# Sim -> Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. +> Sim is the open-source platform to build AI agents and run your agentic workforce. Connect integrations and LLMs to deploy and orchestrate agentic workflows. -Sim lets teams create agents, workflows, knowledge bases, tables, and docs. Over 100,000 builders use Sim — from startups to Fortune 500 companies. SOC2 compliant. +Sim lets teams create agents, workflows, knowledge bases, tables, and docs. It supports both product discovery pages and deeper technical documentation. -## Core Pages +## Preferred URLs -- [Homepage](${baseUrl}): Product overview, features, and pricing +- [Homepage](${baseUrl}): Product overview and primary entry point +- [Integrations directory](${baseUrl}/integrations): Public catalog of integrations and automation capabilities +- [Models directory](${baseUrl}/models): Public catalog of AI models, pricing, context windows, and capabilities +- [Blog](${baseUrl}/blog): Announcements, guides, and product context - [Changelog](${baseUrl}/changelog): Product updates and release notes -- [Sim Blog](${baseUrl}/blog): Announcements, insights, and guides ## Documentation -- [Documentation](https://docs.sim.ai): Complete guides and API reference -- [Quickstart](https://docs.sim.ai/quickstart): Get started in 5 minutes -- [API Reference](https://docs.sim.ai/api): REST API documentation +- [Documentation](https://docs.sim.ai): Product guides and technical reference +- [Quickstart](https://docs.sim.ai/quickstart): Fastest path to getting started +- [API Reference](https://docs.sim.ai/api): API documentation ## Key Concepts - **Workspace**: Container for workflows, data sources, and executions - **Workflow**: Directed graph of blocks defining an agentic process -- **Block**: Individual step (LLM call, tool call, HTTP request, code execution) +- **Block**: Individual step such as an LLM call, tool call, HTTP request, or code execution - **Trigger**: Event or schedule that initiates workflow execution - **Execution**: A single run of a workflow with logs and outputs -- **Knowledge Base**: Vector-indexed document store for retrieval-augmented generation +- **Knowledge Base**: Document store used for retrieval-augmented generation ## Capabilities - AI agent creation and deployment - Agentic workflow orchestration -- 1,000+ integrations (Slack, Gmail, Notion, Airtable, databases, and more) -- Multi-model LLM orchestration (OpenAI, Anthropic, Google, Mistral, xAI, Perplexity) -- Knowledge base creation with retrieval-augmented generation (RAG) +- Integrations across business tools, databases, and communication platforms +- Multi-model LLM orchestration +- Knowledge bases and retrieval-augmented generation - Table creation and management - Document creation and processing - Scheduled and webhook-triggered executions @@ -45,24 +47,19 @@ Sim lets teams create agents, workflows, knowledge bases, tables, and docs. Over - AI agent deployment and orchestration - Knowledge bases and RAG pipelines -- Document creation and processing - Customer support automation -- Internal operations (sales, marketing, legal, finance) +- Internal operations workflows across sales, marketing, legal, and finance -## Links +## Additional Links - [GitHub Repository](https://github.com/simstudioai/sim): Open-source codebase -- [Discord Community](https://discord.gg/Hr4UWYEcTT): Get help and connect with 100,000+ builders -- [X/Twitter](https://x.com/simdotai): Product updates and announcements - -## Optional - -- [Careers](https://jobs.ashbyhq.com/sim): Join the Sim team +- [Docs](https://docs.sim.ai): Canonical documentation source - [Terms of Service](${baseUrl}/terms): Legal terms - [Privacy Policy](${baseUrl}/privacy): Data handling practices +- [Sitemap](${baseUrl}/sitemap.xml): Public URL inventory ` - return new Response(llmsContent, { + return new Response(content, { headers: { 'Content-Type': 'text/markdown; charset=utf-8', 'Cache-Control': 'public, max-age=86400, s-maxage=86400', diff --git a/apps/sim/app/sitemap.ts b/apps/sim/app/sitemap.ts index 6c95b859370..a558525950e 100644 --- a/apps/sim/app/sitemap.ts +++ b/apps/sim/app/sitemap.ts @@ -8,6 +8,34 @@ export default async function sitemap(): Promise { const baseUrl = getBaseUrl() const now = new Date() + const integrationPages: MetadataRoute.Sitemap = integrations.map((integration) => ({ + url: `${baseUrl}/integrations/${integration.slug}`, + lastModified: now, + })) + const modelHubPages: MetadataRoute.Sitemap = [ + { + url: `${baseUrl}/integrations`, + lastModified: now, + }, + { + url: `${baseUrl}/models`, + lastModified: now, + }, + { + url: `${baseUrl}/partners`, + lastModified: now, + }, + ] + const providerPages: MetadataRoute.Sitemap = MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => ({ + url: `${baseUrl}${provider.href}`, + lastModified: new Date( + Math.max(...provider.models.map((model) => new Date(model.pricing.updatedAt).getTime())) + ), + })) + const modelPages: MetadataRoute.Sitemap = ALL_CATALOG_MODELS.map((model) => ({ + url: `${baseUrl}${model.href}`, + lastModified: new Date(model.pricing.updatedAt), + })) const staticPages: MetadataRoute.Sitemap = [ { @@ -26,14 +54,6 @@ export default async function sitemap(): Promise { // url: `${baseUrl}/templates`, // lastModified: now, // }, - { - url: `${baseUrl}/integrations`, - lastModified: now, - }, - { - url: `${baseUrl}/models`, - lastModified: now, - }, { url: `${baseUrl}/changelog`, lastModified: now, @@ -54,20 +74,12 @@ export default async function sitemap(): Promise { lastModified: new Date(p.updated ?? p.date), })) - const integrationPages: MetadataRoute.Sitemap = integrations.map((i) => ({ - url: `${baseUrl}/integrations/${i.slug}`, - lastModified: now, - })) - - const providerPages: MetadataRoute.Sitemap = MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => ({ - url: `${baseUrl}${provider.href}`, - lastModified: now, - })) - - const modelPages: MetadataRoute.Sitemap = ALL_CATALOG_MODELS.map((model) => ({ - url: `${baseUrl}${model.href}`, - lastModified: new Date(model.pricing.updatedAt), - })) - - return [...staticPages, ...blogPages, ...integrationPages, ...providerPages, ...modelPages] + return [ + ...staticPages, + ...modelHubPages, + ...integrationPages, + ...providerPages, + ...modelPages, + ...blogPages, + ] } diff --git a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx index cda3f8c9ccb..77607befa95 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx @@ -1,22 +1,59 @@ 'use client' import { useCallback, useEffect, useRef, useState } from 'react' -import { Check, Copy, Ellipsis, Hash } from 'lucide-react' import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + Button, + Check, + Copy, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Textarea, + ThumbsDown, + ThumbsUp, } from '@/components/emcn' +import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback' + +const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file' + +function toPlainText(raw: string): string { + return ( + raw + // Strip special tags and their contents + .replace(new RegExp(`<\\/?(${SPECIAL_TAGS})(?:>[\\s\\S]*?<\\/(${SPECIAL_TAGS})>|>)`, 'g'), '') + // Strip markdown + .replace(/^#{1,6}\s+/gm, '') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/`{3}[\s\S]*?`{3}/g, '') + .replace(/`(.+?)`/g, '$1') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/^[>\-*]\s+/gm, '') + .replace(/!\[[^\]]*\]\([^)]+\)/g, '') + // Normalize whitespace + .replace(/\n{3,}/g, '\n\n') + .trim() + ) +} + +const ICON_CLASS = 'h-[14px] w-[14px]' +const BUTTON_CLASS = + 'flex h-[26px] w-[26px] items-center justify-center rounded-[6px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-hover)] focus-visible:outline-none' interface MessageActionsProps { content: string - requestId?: string + chatId?: string + userQuery?: string } -export function MessageActions({ content, requestId }: MessageActionsProps) { - const [copied, setCopied] = useState<'message' | 'request' | null>(null) +export function MessageActions({ content, chatId, userQuery }: MessageActionsProps) { + const [copied, setCopied] = useState(false) + const [pendingFeedback, setPendingFeedback] = useState<'up' | 'down' | null>(null) + const [feedbackText, setFeedbackText] = useState('') const resetTimeoutRef = useRef(null) + const submitFeedback = useSubmitCopilotFeedback() useEffect(() => { return () => { @@ -26,59 +63,119 @@ export function MessageActions({ content, requestId }: MessageActionsProps) { } }, []) - const copyToClipboard = useCallback(async (text: string, type: 'message' | 'request') => { + const copyToClipboard = useCallback(async () => { + if (!content) return + const text = toPlainText(content) + if (!text) return try { await navigator.clipboard.writeText(text) - setCopied(type) + setCopied(true) if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current) } - resetTimeoutRef.current = window.setTimeout(() => setCopied(null), 1500) + resetTimeoutRef.current = window.setTimeout(() => setCopied(false), 1500) } catch { + /* clipboard unavailable */ + } + }, [content]) + + const handleFeedbackClick = useCallback( + (type: 'up' | 'down') => { + if (chatId && userQuery) { + setPendingFeedback(type) + setFeedbackText('') + } + }, + [chatId, userQuery] + ) + + const handleSubmitFeedback = useCallback(() => { + if (!pendingFeedback || !chatId || !userQuery) return + const text = feedbackText.trim() + if (!text) { + setPendingFeedback(null) + setFeedbackText('') return } + submitFeedback.mutate({ + chatId, + userQuery, + agentResponse: content, + isPositiveFeedback: pendingFeedback === 'up', + feedback: text, + }) + setPendingFeedback(null) + setFeedbackText('') + }, [pendingFeedback, chatId, userQuery, content, feedbackText]) + + const handleModalClose = useCallback((open: boolean) => { + if (!open) { + setPendingFeedback(null) + setFeedbackText('') + } }, []) - if (!content && !requestId) { - return null - } + if (!content) return null return ( - - + <> +
- - - { - event.stopPropagation() - void copyToClipboard(content, 'message') - }} + + +
+ + + + Give feedback + +
+

+ {pendingFeedback === 'up' ? 'What did you like?' : 'What could be improved?'} +

+