mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
feat(whitelabel): add in the ability to whitelabel via envvars (#887)
* feat(whitelabel): add in the ability to whitelabel via envvars * restore site.webmanifest * fix(dynamic): remove force-dynamic from routes that don't need it (#888) * Reinstall dependencies * Update docs
This commit is contained in:
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -537,7 +537,7 @@ This visibility system ensures clean user interfaces while maintaining full flex
|
||||
|
||||
### Guidelines & Best Practices
|
||||
|
||||
- **Code Style:** Follow the project's ESLint and Prettier configurations. Use meaningful variable names and small, focused functions.
|
||||
- **Code Style:** Follow the project's Biome configurations. Use meaningful variable names and small, focused functions.
|
||||
- **Documentation:** Clearly document the purpose, inputs, outputs, and any special behavior for your block/tool.
|
||||
- **Error Handling:** Implement robust error handling and provide user-friendly error messages.
|
||||
- **Parameter Visibility:** Always specify the appropriate visibility level for each parameter to ensure proper UI behavior and LLM integration.
|
||||
|
||||
@@ -50,7 +50,7 @@ The File Parser tool is particularly useful for scenarios where your agents need
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Upload and extract contents from structured file formats including PDFs, CSV spreadsheets, and Word documents (DOCX). Upload files directly. Specialized parsers extract text and metadata from each format. You can upload multiple files at once and access them individually or as a combined document.
|
||||
Upload and extract contents from structured file formats including PDFs, CSV spreadsheets, and Word documents (DOCX). You can either provide a URL to a file or upload files directly. Specialized parsers extract text and metadata from each format. You can upload multiple files at once and access them individually or as a combined document.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ With Hunter.io, you can:
|
||||
In Sim, the Hunter.io integration enables your agents to programmatically search for and verify email addresses, discover companies, and enrich contact data using Hunter.io’s API. This allows you to automate lead generation, contact enrichment, and email verification directly within your workflows. Your agents can leverage Hunter.io’s tools to streamline outreach, keep your CRM up-to-date, and power intelligent automation scenarios for sales, recruiting, and more.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Search for email addresses, verify their deliverability, discover companies, and enrich contact data using Hunter.io's powerful email finding capabilities.
|
||||
|
||||
@@ -64,7 +64,7 @@ Search for similar content in a knowledge base using vector similarity
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `knowledgeBaseId` | string | Yes | ID of the knowledge base to search in |
|
||||
| `query` | string | Yes | Search query text |
|
||||
| `query` | string | No | Search query text \(optional when using tag filters\) |
|
||||
| `topK` | number | No | Number of most similar results to return \(1-100\) |
|
||||
| `tagFilters` | any | No | Array of tag filters with tagName and tagValue properties |
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ The Mistral Parse tool is particularly useful for scenarios where your agents ne
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Extract text and structure from PDF documents using Mistral's OCR API. Configure processing options and get the content in your preferred format. For URLs, they must be publicly accessible and point to a valid PDF file. Note: Google Drive, Dropbox, and other cloud storage links are not supported; use a direct download URL from a web server instead.
|
||||
Extract text and structure from PDF documents using Mistral's OCR API. Either enter a URL to a PDF document or upload a PDF file directly. Configure processing options and get the content in your preferred format. For URLs, they must be publicly accessible and point to a valid PDF file. Note: Google Drive, Dropbox, and other cloud storage links are not supported; use a direct download URL from a web server instead.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -158,6 +158,10 @@ Send emails using Outlook
|
||||
| `to` | string | Yes | Recipient email address |
|
||||
| `subject` | string | Yes | Email subject |
|
||||
| `body` | string | Yes | Email body content |
|
||||
| `replyToMessageId` | string | No | Message ID to reply to \(for threading\) |
|
||||
| `conversationId` | string | No | Conversation ID for threading |
|
||||
| `cc` | string | No | CC recipients \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipients \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import { GridPattern } from '@/app/(landing)/components/grid-pattern'
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const brand = useBrandConfig()
|
||||
|
||||
return (
|
||||
<main className='relative flex min-h-screen flex-col bg-[#0C0C0C] font-geist-sans text-white'>
|
||||
{/* Background pattern */}
|
||||
@@ -21,7 +24,17 @@ export default function AuthLayout({ children }: { children: React.ReactNode })
|
||||
<div className='relative z-10 px-6 pt-9'>
|
||||
<div className='mx-auto max-w-7xl'>
|
||||
<Link href='/' className='inline-flex'>
|
||||
<Image src='/sim.svg' alt='Sim Logo' width={42} height={42} />
|
||||
{brand.logoUrl ? (
|
||||
<img
|
||||
src={brand.logoUrl}
|
||||
alt={`${brand.name} Logo`}
|
||||
width={42}
|
||||
height={42}
|
||||
className='h-[42px] w-[42px] object-contain'
|
||||
/>
|
||||
) : (
|
||||
<Image src='/sim.svg' alt={`${brand.name} Logo`} width={42} height={42} />
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import { usePrefetchOnHover } from '@/app/(landing)/utils/prefetch'
|
||||
|
||||
// --- Framer Motion Variants ---
|
||||
@@ -165,6 +166,7 @@ export default function NavClient({
|
||||
const [isMobile, setIsMobile] = useState(initialIsMobile ?? false)
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false)
|
||||
const _router = useRouter()
|
||||
const brand = useBrandConfig()
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
@@ -199,7 +201,17 @@ export default function NavClient({
|
||||
<div className='flex flex-1 items-center'>
|
||||
<div className='inline-block'>
|
||||
<Link href='/' className='inline-flex'>
|
||||
<Image src='/sim.svg' alt='Sim Logo' width={42} height={42} />
|
||||
{brand.logoUrl ? (
|
||||
<img
|
||||
src={brand.logoUrl}
|
||||
alt={`${brand.name} Logo`}
|
||||
width={42}
|
||||
height={42}
|
||||
className='h-[42px] w-[42px] object-contain'
|
||||
/>
|
||||
) : (
|
||||
<Image src='/sim.svg' alt={`${brand.name} Logo`} width={42} height={42} />
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Analytics } from '@vercel/analytics/next'
|
||||
import { SpeedInsights } from '@vercel/speed-insights/next'
|
||||
import { GeistSans } from 'geist/font/sans'
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
import { PublicEnvScript } from 'next-runtime-env'
|
||||
import { BrandedLayout } from '@/components/branded-layout'
|
||||
import { generateBrandedMetadata, generateStructuredData } from '@/lib/branding/metadata'
|
||||
import { env } from '@/lib/env'
|
||||
import { isHosted } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getAssetUrl } from '@/lib/utils'
|
||||
@@ -51,149 +53,20 @@ export const viewport: Viewport = {
|
||||
userScalable: false,
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '',
|
||||
default: 'Sim',
|
||||
},
|
||||
description:
|
||||
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
|
||||
applicationName: 'Sim',
|
||||
authors: [{ name: 'Sim' }],
|
||||
generator: 'Next.js',
|
||||
keywords: [
|
||||
'AI agent',
|
||||
'AI agent builder',
|
||||
'AI agent workflow',
|
||||
'AI workflow automation',
|
||||
'visual workflow editor',
|
||||
'AI agents',
|
||||
'workflow canvas',
|
||||
'intelligent automation',
|
||||
'AI tools',
|
||||
'workflow designer',
|
||||
'artificial intelligence',
|
||||
'business automation',
|
||||
'AI agent workflows',
|
||||
'visual programming',
|
||||
],
|
||||
referrer: 'origin-when-cross-origin',
|
||||
creator: 'Sim',
|
||||
publisher: 'Sim',
|
||||
metadataBase: new URL('https://sim.ai'),
|
||||
alternates: {
|
||||
canonical: '/',
|
||||
languages: {
|
||||
'en-US': '/en-US',
|
||||
},
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-image-preview': 'large',
|
||||
'max-video-preview': -1,
|
||||
'max-snippet': -1,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
url: 'https://sim.ai',
|
||||
title: 'Sim',
|
||||
description:
|
||||
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
|
||||
siteName: 'Sim',
|
||||
images: [
|
||||
{
|
||||
url: getAssetUrl('social/facebook.png'),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Sim',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Sim',
|
||||
description:
|
||||
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
|
||||
images: [getAssetUrl('social/twitter.png')],
|
||||
creator: '@simstudioai',
|
||||
site: '@simstudioai',
|
||||
},
|
||||
manifest: '/favicon/site.webmanifest',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
|
||||
{ url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
|
||||
{
|
||||
url: '/favicon/favicon-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
url: '/favicon/favicon-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{ url: '/sim.png', sizes: 'any', type: 'image/png' },
|
||||
],
|
||||
apple: '/favicon/apple-touch-icon.png',
|
||||
shortcut: '/favicon/favicon.ico',
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: 'default',
|
||||
title: 'Sim',
|
||||
},
|
||||
formatDetection: {
|
||||
telephone: false,
|
||||
},
|
||||
category: 'technology',
|
||||
other: {
|
||||
'apple-mobile-web-app-capable': 'yes',
|
||||
'mobile-web-app-capable': 'yes',
|
||||
'msapplication-TileColor': '#ffffff',
|
||||
'msapplication-config': '/favicon/browserconfig.xml',
|
||||
},
|
||||
}
|
||||
// Generate dynamic metadata based on brand configuration
|
||||
export const metadata: Metadata = generateBrandedMetadata()
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const structuredData = generateStructuredData()
|
||||
|
||||
return (
|
||||
<html lang='en' suppressHydrationWarning className={GeistSans.className}>
|
||||
<html lang='en' suppressHydrationWarning>
|
||||
<head>
|
||||
{/* Structured Data for SEO */}
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Sim',
|
||||
description:
|
||||
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
|
||||
url: 'https://sim.ai',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
category: 'SaaS',
|
||||
},
|
||||
creator: {
|
||||
'@type': 'Organization',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
},
|
||||
featureList: [
|
||||
'Visual AI Agent Builder',
|
||||
'Workflow Canvas Interface',
|
||||
'AI Agent Automation',
|
||||
'Custom AI Workflows',
|
||||
],
|
||||
}),
|
||||
__html: JSON.stringify(structuredData),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -226,24 +99,26 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<PublicEnvScript />
|
||||
|
||||
{/* RB2B Script - Only load on hosted version */}
|
||||
{/* {isHosted && env.NEXT_PUBLIC_RB2B_KEY && (
|
||||
{isHosted && env.NEXT_PUBLIC_RB2B_KEY && (
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `!function () {var reb2b = window.reb2b = window.reb2b || [];if (reb2b.invoked) return;reb2b.invoked = true;reb2b.methods = ["identify", "collect"];reb2b.factory = function (method) {return function () {var args = Array.prototype.slice.call(arguments);args.unshift(method);reb2b.push(args);return reb2b;};};for (var i = 0; i < reb2b.methods.length; i++) {var key = reb2b.methods[i];reb2b[key] = reb2b.factory(key);}reb2b.load = function (key) {var script = document.createElement("script");script.type = "text/javascript";script.async = true;script.src = "https://b2bjsstore.s3.us-west-2.amazonaws.com/b/" + key + "/${env.NEXT_PUBLIC_RB2B_KEY}.js.gz";var first = document.getElementsByTagName("script")[0];first.parentNode.insertBefore(script, first);};reb2b.SNIPPET_VERSION = "1.0.1";reb2b.load("${env.NEXT_PUBLIC_RB2B_KEY}");}();`,
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
)}
|
||||
</head>
|
||||
<body suppressHydrationWarning>
|
||||
<ZoomPrevention />
|
||||
<TelemetryConsentDialog />
|
||||
{children}
|
||||
{isHosted && (
|
||||
<>
|
||||
<SpeedInsights />
|
||||
<Analytics />
|
||||
</>
|
||||
)}
|
||||
<BrandedLayout>
|
||||
<ZoomPrevention />
|
||||
<TelemetryConsentDialog />
|
||||
{children}
|
||||
{isHosted && (
|
||||
<>
|
||||
<SpeedInsights />
|
||||
<Analytics />
|
||||
</>
|
||||
)}
|
||||
</BrandedLayout>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
29
apps/sim/app/manifest.ts
Normal file
29
apps/sim/app/manifest.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { MetadataRoute } from 'next'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
const brand = getBrandConfig()
|
||||
|
||||
return {
|
||||
name: brand.name,
|
||||
short_name: brand.name,
|
||||
description:
|
||||
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: brand.primaryColor || '#ffffff',
|
||||
theme_color: brand.primaryColor || '#ffffff',
|
||||
icons: [
|
||||
{
|
||||
src: '/favicon/android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: '/favicon/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Suspense, useEffect, useState } from 'react'
|
||||
import { CheckCircle, Heart, Info, Loader2, XCircle } from 'lucide-react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
|
||||
interface UnsubscribeData {
|
||||
success: boolean
|
||||
@@ -26,6 +27,7 @@ function UnsubscribeContent() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const [unsubscribed, setUnsubscribed] = useState(false)
|
||||
const brand = useBrandConfig()
|
||||
|
||||
const email = searchParams.get('email')
|
||||
const token = searchParams.get('token')
|
||||
@@ -160,7 +162,7 @@ function UnsubscribeContent() {
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open(
|
||||
'mailto:help@sim.ai?subject=Unsubscribe%20Help&body=Hi%2C%20I%20need%20help%20unsubscribing%20from%20emails.%20My%20unsubscribe%20link%20is%20not%20working.',
|
||||
`mailto:${brand.supportEmail}?subject=Unsubscribe%20Help&body=Hi%2C%20I%20need%20help%20unsubscribing%20from%20emails.%20My%20unsubscribe%20link%20is%20not%20working.`,
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
@@ -176,8 +178,8 @@ function UnsubscribeContent() {
|
||||
<div className='mt-4 text-center'>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Need immediate help? Email us at{' '}
|
||||
<a href='mailto:help@sim.ai' className='text-primary hover:underline'>
|
||||
help@sim.ai
|
||||
<a href={`mailto:${brand.supportEmail}`} className='text-primary hover:underline'>
|
||||
{brand.supportEmail}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -222,7 +224,7 @@ function UnsubscribeContent() {
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open(
|
||||
'mailto:help@sim.ai?subject=Account%20Help&body=Hi%2C%20I%20need%20help%20with%20my%20account%20emails.',
|
||||
`mailto:${brand.supportEmail}?subject=Account%20Help&body=Hi%2C%20I%20need%20help%20with%20my%20account%20emails.`,
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
@@ -256,8 +258,8 @@ function UnsubscribeContent() {
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
If you change your mind, you can always update your email preferences in your account
|
||||
settings or contact us at{' '}
|
||||
<a href='mailto:help@sim.ai' className='text-primary hover:underline'>
|
||||
help@sim.ai
|
||||
<a href={`mailto:${brand.supportEmail}`} className='text-primary hover:underline'>
|
||||
{brand.supportEmail}
|
||||
</a>
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -369,8 +371,8 @@ function UnsubscribeContent() {
|
||||
|
||||
<p className='text-center text-muted-foreground text-xs'>
|
||||
Questions? Contact us at{' '}
|
||||
<a href='mailto:help@sim.ai' className='text-primary hover:underline'>
|
||||
help@sim.ai
|
||||
<a href={`mailto:${brand.supportEmail}`} className='text-primary hover:underline'>
|
||||
{brand.supportEmail}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,14 @@ import { BookOpen, Building2, LibraryBig, ScrollText, Search, Shapes, Workflow }
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Dialog, DialogOverlay, DialogPortal, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
TemplateCard,
|
||||
TemplateCardSkeleton,
|
||||
} from '@/app/workspace/[workspaceId]/templates/components/template-card'
|
||||
import { getKeyboardShortcutText } from '@/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import { TemplateCard, TemplateCardSkeleton } from '../../../templates/components/template-card'
|
||||
import { getKeyboardShortcutText } from '../../hooks/use-keyboard-shortcuts'
|
||||
import { type NavigationSection, useSearchNavigation } from './hooks/use-search-navigation'
|
||||
|
||||
interface SearchModalProps {
|
||||
@@ -100,6 +104,7 @@ export function SearchModal({
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const brand = useBrandConfig()
|
||||
|
||||
// Local state for templates to handle star changes
|
||||
const [localTemplates, setLocalTemplates] = useState<TemplateData[]>(templates)
|
||||
@@ -182,7 +187,7 @@ export function SearchModal({
|
||||
id: 'docs',
|
||||
name: 'Docs',
|
||||
icon: BookOpen,
|
||||
href: 'https://docs.simstudio.ai/',
|
||||
href: brand.documentationUrl || 'https://docs.sim.ai/',
|
||||
},
|
||||
],
|
||||
[workspaceId]
|
||||
|
||||
55
apps/sim/components/branded-layout.tsx
Normal file
55
apps/sim/components/branded-layout.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { generateBrandCSS, getBrandConfig } from '@/lib/branding/branding'
|
||||
|
||||
interface BrandedLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function BrandedLayout({ children }: BrandedLayoutProps) {
|
||||
useEffect(() => {
|
||||
const config = getBrandConfig()
|
||||
|
||||
// Update document title
|
||||
if (config.name !== 'Sim') {
|
||||
document.title = config.name
|
||||
}
|
||||
|
||||
// Update favicon
|
||||
if (config.faviconUrl) {
|
||||
const faviconLink = document.querySelector("link[rel*='icon']") as HTMLLinkElement
|
||||
if (faviconLink) {
|
||||
faviconLink.href = config.faviconUrl
|
||||
}
|
||||
}
|
||||
|
||||
// Inject brand CSS
|
||||
const brandStyleId = 'brand-styles'
|
||||
let brandStyleElement = document.getElementById(brandStyleId) as HTMLStyleElement
|
||||
|
||||
if (!brandStyleElement) {
|
||||
brandStyleElement = document.createElement('style')
|
||||
brandStyleElement.id = brandStyleId
|
||||
document.head.appendChild(brandStyleElement)
|
||||
}
|
||||
|
||||
brandStyleElement.textContent = generateBrandCSS(config)
|
||||
|
||||
// Load custom CSS if provided
|
||||
if (config.customCssUrl) {
|
||||
const customCssId = 'custom-brand-css'
|
||||
let customCssLink = document.getElementById(customCssId) as HTMLLinkElement
|
||||
|
||||
if (!customCssLink) {
|
||||
customCssLink = document.createElement('link')
|
||||
customCssLink.id = customCssId
|
||||
customCssLink.rel = 'stylesheet'
|
||||
customCssLink.href = config.customCssUrl
|
||||
document.head.appendChild(customCssLink)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
|
||||
interface WorkspaceInvitation {
|
||||
workspaceId: string
|
||||
@@ -57,6 +58,7 @@ export const BatchInvitationEmail = ({
|
||||
workspaceInvitations = [],
|
||||
acceptUrl,
|
||||
}: BatchInvitationEmailProps) => {
|
||||
const brand = getBrandConfig()
|
||||
const hasWorkspaces = workspaceInvitations.length > 0
|
||||
|
||||
return (
|
||||
@@ -69,7 +71,13 @@ export const BatchInvitationEmail = ({
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Section style={logoContainer}>
|
||||
<Img src='https://sim.ai/logo.png' width='120' height='36' alt='Sim' style={logo} />
|
||||
<Img
|
||||
src={brand.logoUrl || 'https://sim.ai/logo.png'}
|
||||
width='120'
|
||||
height='36'
|
||||
alt={brand.name}
|
||||
style={logo}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Heading style={h1}>You're invited to join {organizationName}!</Heading>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Container, Img, Link, Section, Text } from '@react-email/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { env } from '@/lib/env'
|
||||
import { getAssetUrl } from '@/lib/utils'
|
||||
|
||||
@@ -16,6 +17,8 @@ export const EmailFooter = ({
|
||||
baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai',
|
||||
unsubscribe,
|
||||
}: EmailFooterProps) => {
|
||||
const brand = getBrandConfig()
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Section style={{ maxWidth: '580px', margin: '0 auto', padding: '20px 0' }}>
|
||||
@@ -62,11 +65,11 @@ export const EmailFooter = ({
|
||||
margin: '8px 0 0 0',
|
||||
}}
|
||||
>
|
||||
© {new Date().getFullYear()} Sim, All Rights Reserved
|
||||
© {new Date().getFullYear()} {brand.name}, All Rights Reserved
|
||||
<br />
|
||||
If you have any questions, please contact us at{' '}
|
||||
<a
|
||||
href='mailto:help@sim.ai'
|
||||
href={`mailto:${brand.supportEmail}`}
|
||||
style={{
|
||||
color: '#706a7b !important',
|
||||
textDecoration: 'underline',
|
||||
@@ -74,7 +77,7 @@ export const EmailFooter = ({
|
||||
fontFamily: 'HelveticaNeue, Helvetica, Arial, sans-serif',
|
||||
}}
|
||||
>
|
||||
help@sim.ai
|
||||
{brand.supportEmail}
|
||||
</a>
|
||||
</Text>
|
||||
<table cellPadding={0} cellSpacing={0} style={{ width: '100%', marginTop: '4px' }}>
|
||||
@@ -118,7 +121,7 @@ export const EmailFooter = ({
|
||||
href={
|
||||
unsubscribe?.unsubscribeToken && unsubscribe?.email
|
||||
? `${baseUrl}/unsubscribe?token=${unsubscribe.unsubscribeToken}&email=${encodeURIComponent(unsubscribe.email)}`
|
||||
: `mailto:help@sim.ai?subject=Unsubscribe%20Request&body=Please%20unsubscribe%20me%20from%20all%20emails.`
|
||||
: `mailto:${brand.supportEmail}?subject=Unsubscribe%20Request&body=Please%20unsubscribe%20me%20from%20all%20emails.`
|
||||
}
|
||||
style={{
|
||||
color: '#706a7b !important',
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { format } from 'date-fns'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { env } from '@/lib/env'
|
||||
import { getAssetUrl } from '@/lib/utils'
|
||||
import { baseStyles } from './base-styles'
|
||||
@@ -34,6 +35,8 @@ export const InvitationEmail = ({
|
||||
invitedEmail = '',
|
||||
updatedDate = new Date(),
|
||||
}: InvitationEmailProps) => {
|
||||
const brand = getBrandConfig()
|
||||
|
||||
// Extract invitation ID or token from inviteLink if present
|
||||
let enhancedLink = inviteLink
|
||||
|
||||
@@ -60,9 +63,9 @@ export const InvitationEmail = ({
|
||||
<Row>
|
||||
<Column style={{ textAlign: 'center' }}>
|
||||
<Img
|
||||
src={getAssetUrl('static/sim.png')}
|
||||
src={brand.logoUrl || getAssetUrl('static/sim.png')}
|
||||
width='114'
|
||||
alt='Sim'
|
||||
alt={brand.name}
|
||||
style={{
|
||||
margin: '0 auto',
|
||||
}}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { env } from '@/lib/env'
|
||||
import { getAssetUrl } from '@/lib/utils'
|
||||
import { baseStyles } from './base-styles'
|
||||
@@ -24,18 +25,18 @@ interface OTPVerificationEmailProps {
|
||||
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
|
||||
|
||||
const getSubjectByType = (type: string, chatTitle?: string) => {
|
||||
const getSubjectByType = (type: string, brandName: string, chatTitle?: string) => {
|
||||
switch (type) {
|
||||
case 'sign-in':
|
||||
return 'Sign in to Sim'
|
||||
return `Sign in to ${brandName}`
|
||||
case 'email-verification':
|
||||
return 'Verify your email for Sim'
|
||||
return `Verify your email for ${brandName}`
|
||||
case 'forget-password':
|
||||
return 'Reset your Sim password'
|
||||
return `Reset your ${brandName} password`
|
||||
case 'chat-access':
|
||||
return `Verification code for ${chatTitle || 'Chat'}`
|
||||
default:
|
||||
return 'Verification code for Sim'
|
||||
return `Verification code for ${brandName}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,17 +46,19 @@ export const OTPVerificationEmail = ({
|
||||
type = 'email-verification',
|
||||
chatTitle,
|
||||
}: OTPVerificationEmailProps) => {
|
||||
const brand = getBrandConfig()
|
||||
|
||||
// Get a message based on the type
|
||||
const getMessage = () => {
|
||||
switch (type) {
|
||||
case 'sign-in':
|
||||
return 'Sign in to Sim'
|
||||
return `Sign in to ${brand.name}`
|
||||
case 'forget-password':
|
||||
return 'Reset your password for Sim'
|
||||
return `Reset your password for ${brand.name}`
|
||||
case 'chat-access':
|
||||
return `Access ${chatTitle || 'the chat'}`
|
||||
default:
|
||||
return 'Welcome to Sim'
|
||||
return `Welcome to ${brand.name}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,15 +66,15 @@ export const OTPVerificationEmail = ({
|
||||
<Html>
|
||||
<Head />
|
||||
<Body style={baseStyles.main}>
|
||||
<Preview>{getSubjectByType(type, chatTitle)}</Preview>
|
||||
<Preview>{getSubjectByType(type, brand.name, chatTitle)}</Preview>
|
||||
<Container style={baseStyles.container}>
|
||||
<Section style={{ padding: '30px 0', textAlign: 'center' }}>
|
||||
<Row>
|
||||
<Column style={{ textAlign: 'center' }}>
|
||||
<Img
|
||||
src={getAssetUrl('static/sim.png')}
|
||||
src={brand.logoUrl || getAssetUrl('static/sim.png')}
|
||||
width='114'
|
||||
alt='Sim'
|
||||
alt={brand.name}
|
||||
style={{
|
||||
margin: '0 auto',
|
||||
}}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { format } from 'date-fns'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { env } from '@/lib/env'
|
||||
import { getAssetUrl } from '@/lib/utils'
|
||||
import { baseStyles } from './base-styles'
|
||||
@@ -30,19 +31,21 @@ export const ResetPasswordEmail = ({
|
||||
resetLink = '',
|
||||
updatedDate = new Date(),
|
||||
}: ResetPasswordEmailProps) => {
|
||||
const brand = getBrandConfig()
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Body style={baseStyles.main}>
|
||||
<Preview>Reset your Sim password</Preview>
|
||||
<Preview>Reset your {brand.name} password</Preview>
|
||||
<Container style={baseStyles.container}>
|
||||
<Section style={{ padding: '30px 0', textAlign: 'center' }}>
|
||||
<Row>
|
||||
<Column style={{ textAlign: 'center' }}>
|
||||
<Img
|
||||
src={getAssetUrl('static/sim.png')}
|
||||
src={brand.logoUrl || getAssetUrl('static/sim.png')}
|
||||
width='114'
|
||||
alt='Sim'
|
||||
alt={brand.name}
|
||||
style={{
|
||||
margin: '0 auto',
|
||||
}}
|
||||
@@ -62,8 +65,8 @@ export const ResetPasswordEmail = ({
|
||||
<Section style={baseStyles.content}>
|
||||
<Text style={baseStyles.paragraph}>Hello {username},</Text>
|
||||
<Text style={baseStyles.paragraph}>
|
||||
You recently requested to reset your password for your Sim account. Use the button
|
||||
below to reset it. This password reset is only valid for the next 24 hours.
|
||||
You recently requested to reset your password for your {brand.name} account. Use the
|
||||
button below to reset it. This password reset is only valid for the next 24 hours.
|
||||
</Text>
|
||||
<Link href={resetLink} style={{ textDecoration: 'none' }}>
|
||||
<Text style={baseStyles.button}>Reset Your Password</Text>
|
||||
@@ -75,7 +78,7 @@ export const ResetPasswordEmail = ({
|
||||
<Text style={baseStyles.paragraph}>
|
||||
Best regards,
|
||||
<br />
|
||||
The Sim Team
|
||||
The {brand.name} Team
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { env } from '@/lib/env'
|
||||
import { getAssetUrl } from '@/lib/utils'
|
||||
import { baseStyles } from './base-styles'
|
||||
@@ -29,6 +30,8 @@ export const WorkspaceInvitationEmail = ({
|
||||
inviterName = 'Someone',
|
||||
invitationLink = '',
|
||||
}: WorkspaceInvitationEmailProps) => {
|
||||
const brand = getBrandConfig()
|
||||
|
||||
// Extract token from the link to ensure we're using the correct format
|
||||
let enhancedLink = invitationLink
|
||||
|
||||
@@ -55,9 +58,9 @@ export const WorkspaceInvitationEmail = ({
|
||||
<Row>
|
||||
<Column style={{ textAlign: 'center' }}>
|
||||
<Img
|
||||
src={getAssetUrl('static/sim.png')}
|
||||
src={brand.logoUrl || getAssetUrl('static/sim.png')}
|
||||
width='114'
|
||||
alt='Sim'
|
||||
alt={brand.name}
|
||||
style={{
|
||||
margin: '0 auto',
|
||||
}}
|
||||
|
||||
81
apps/sim/lib/branding/branding.ts
Normal file
81
apps/sim/lib/branding/branding.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { getEnv } from '@/lib/env'
|
||||
|
||||
export interface BrandConfig {
|
||||
name: string
|
||||
logoUrl?: string
|
||||
faviconUrl?: string
|
||||
primaryColor?: string
|
||||
secondaryColor?: string
|
||||
accentColor?: string
|
||||
customCssUrl?: string
|
||||
hideBranding?: boolean
|
||||
footerText?: string
|
||||
supportEmail?: string
|
||||
supportUrl?: string
|
||||
documentationUrl?: string
|
||||
termsUrl?: string
|
||||
privacyUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Default brand configuration values
|
||||
*/
|
||||
const defaultConfig: BrandConfig = {
|
||||
name: 'Sim',
|
||||
logoUrl: undefined,
|
||||
faviconUrl: '/favicon/favicon.ico',
|
||||
primaryColor: '#000000',
|
||||
secondaryColor: '#6366f1',
|
||||
accentColor: '#f59e0b',
|
||||
customCssUrl: undefined,
|
||||
hideBranding: false,
|
||||
footerText: undefined,
|
||||
supportEmail: 'help@sim.ai',
|
||||
supportUrl: undefined,
|
||||
documentationUrl: undefined,
|
||||
termsUrl: undefined,
|
||||
privacyUrl: undefined,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get branding configuration from environment variables
|
||||
* Supports runtime configuration via Docker/Kubernetes
|
||||
*/
|
||||
export const getBrandConfig = (): BrandConfig => {
|
||||
return {
|
||||
name: getEnv('NEXT_PUBLIC_BRAND_NAME') || defaultConfig.name,
|
||||
logoUrl: getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') || defaultConfig.logoUrl,
|
||||
faviconUrl: getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL') || defaultConfig.faviconUrl,
|
||||
primaryColor: getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR') || defaultConfig.primaryColor,
|
||||
secondaryColor: getEnv('NEXT_PUBLIC_BRAND_SECONDARY_COLOR') || defaultConfig.secondaryColor,
|
||||
accentColor: getEnv('NEXT_PUBLIC_BRAND_ACCENT_COLOR') || defaultConfig.accentColor,
|
||||
customCssUrl: getEnv('NEXT_PUBLIC_CUSTOM_CSS_URL') || defaultConfig.customCssUrl,
|
||||
hideBranding: getEnv('NEXT_PUBLIC_HIDE_BRANDING') === 'true',
|
||||
footerText: getEnv('NEXT_PUBLIC_CUSTOM_FOOTER_TEXT') || defaultConfig.footerText,
|
||||
supportEmail: getEnv('NEXT_PUBLIC_SUPPORT_EMAIL') || defaultConfig.supportEmail,
|
||||
supportUrl: getEnv('NEXT_PUBLIC_SUPPORT_URL') || defaultConfig.supportUrl,
|
||||
documentationUrl: getEnv('NEXT_PUBLIC_DOCUMENTATION_URL') || defaultConfig.documentationUrl,
|
||||
termsUrl: getEnv('NEXT_PUBLIC_TERMS_URL') || defaultConfig.termsUrl,
|
||||
privacyUrl: getEnv('NEXT_PUBLIC_PRIVACY_URL') || defaultConfig.privacyUrl,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSS custom properties for brand colors
|
||||
*/
|
||||
export const generateBrandCSS = (config: BrandConfig): string => {
|
||||
return `
|
||||
:root {
|
||||
--brand-primary: ${config.primaryColor};
|
||||
--brand-secondary: ${config.secondaryColor};
|
||||
--brand-accent: ${config.accentColor};
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to use brand configuration in React components
|
||||
*/
|
||||
export const useBrandConfig = () => {
|
||||
return getBrandConfig()
|
||||
}
|
||||
153
apps/sim/lib/branding/metadata.ts
Normal file
153
apps/sim/lib/branding/metadata.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { env } from '@/lib/env'
|
||||
import { getAssetUrl } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* Generate dynamic metadata based on brand configuration
|
||||
*/
|
||||
export function generateBrandedMetadata(override: Partial<Metadata> = {}): Metadata {
|
||||
const brand = getBrandConfig()
|
||||
|
||||
const defaultTitle = brand.name
|
||||
const defaultDescription = `Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.`
|
||||
|
||||
return {
|
||||
title: {
|
||||
template: `%s | ${brand.name}`,
|
||||
default: defaultTitle,
|
||||
},
|
||||
description: defaultDescription,
|
||||
applicationName: brand.name,
|
||||
authors: [{ name: brand.name }],
|
||||
generator: 'Next.js',
|
||||
keywords: [
|
||||
'AI agent',
|
||||
'AI agent builder',
|
||||
'AI agent workflow',
|
||||
'AI workflow automation',
|
||||
'visual workflow editor',
|
||||
'AI agents',
|
||||
'workflow canvas',
|
||||
'intelligent automation',
|
||||
'AI tools',
|
||||
'workflow designer',
|
||||
'artificial intelligence',
|
||||
'business automation',
|
||||
'AI agent workflows',
|
||||
'visual programming',
|
||||
],
|
||||
referrer: 'origin-when-cross-origin',
|
||||
creator: brand.name,
|
||||
publisher: brand.name,
|
||||
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
|
||||
alternates: {
|
||||
canonical: '/',
|
||||
languages: {
|
||||
'en-US': '/en-US',
|
||||
},
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-image-preview': 'large',
|
||||
'max-video-preview': -1,
|
||||
'max-snippet': -1,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
url: env.NEXT_PUBLIC_APP_URL,
|
||||
title: defaultTitle,
|
||||
description: defaultDescription,
|
||||
siteName: brand.name,
|
||||
images: [
|
||||
{
|
||||
url: brand.logoUrl || getAssetUrl('social/facebook.png'),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: brand.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: defaultTitle,
|
||||
description: defaultDescription,
|
||||
images: [brand.logoUrl || getAssetUrl('social/twitter.png')],
|
||||
creator: '@simstudioai',
|
||||
site: '@simstudioai',
|
||||
},
|
||||
manifest: '/manifest.webmanifest',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
|
||||
{ url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
|
||||
{
|
||||
url: '/favicon/favicon-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
url: '/favicon/favicon-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{ url: brand.faviconUrl || '/sim.png', sizes: 'any', type: 'image/png' },
|
||||
],
|
||||
apple: '/favicon/apple-touch-icon.png',
|
||||
shortcut: brand.faviconUrl || '/favicon/favicon.ico',
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: 'default',
|
||||
title: brand.name,
|
||||
},
|
||||
formatDetection: {
|
||||
telephone: false,
|
||||
},
|
||||
category: 'technology',
|
||||
other: {
|
||||
'apple-mobile-web-app-capable': 'yes',
|
||||
'mobile-web-app-capable': 'yes',
|
||||
'msapplication-TileColor': brand.primaryColor || '#ffffff',
|
||||
'msapplication-config': '/favicon/browserconfig.xml',
|
||||
},
|
||||
...override,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate static structured data for SEO
|
||||
*/
|
||||
export function generateStructuredData() {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Sim',
|
||||
description:
|
||||
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
|
||||
url: 'https://sim.ai',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web Browser',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
category: 'SaaS',
|
||||
},
|
||||
creator: {
|
||||
'@type': 'Organization',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
},
|
||||
featureList: [
|
||||
'Visual AI Agent Builder',
|
||||
'Workflow Canvas Interface',
|
||||
'AI Agent Automation',
|
||||
'Custom AI Workflows',
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,22 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_RB2B_KEY: z.string().optional(), // RB2B tracking key for B2B analytics
|
||||
NEXT_PUBLIC_GOOGLE_API_KEY: z.string().optional(), // Google API key for client-side API calls
|
||||
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: z.string().optional(), // Google project number for Drive picker
|
||||
|
||||
// UI Branding & Whitelabeling
|
||||
NEXT_PUBLIC_BRAND_NAME: z.string().optional(), // Custom brand name (defaults to "Sim")
|
||||
NEXT_PUBLIC_BRAND_LOGO_URL: z.string().url().optional(), // Custom logo URL
|
||||
NEXT_PUBLIC_BRAND_FAVICON_URL: z.string().url().optional(), // Custom favicon URL
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: z.string().optional(), // Primary brand color (hex)
|
||||
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: z.string().optional(), // Secondary brand color (hex)
|
||||
NEXT_PUBLIC_BRAND_ACCENT_COLOR: z.string().optional(), // Accent brand color (hex)
|
||||
NEXT_PUBLIC_CUSTOM_CSS_URL: z.string().url().optional(), // Custom CSS stylesheet URL
|
||||
NEXT_PUBLIC_HIDE_BRANDING: z.string().optional(), // Hide "Powered by" branding
|
||||
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: z.string().optional(), // Custom footer text
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email
|
||||
NEXT_PUBLIC_SUPPORT_URL: z.string().url().optional(), // Custom support URL
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL
|
||||
NEXT_PUBLIC_TERMS_URL: z.string().url().optional(), // Custom terms of service URL
|
||||
NEXT_PUBLIC_PRIVACY_URL: z.string().url().optional(), // Custom privacy policy URL
|
||||
},
|
||||
|
||||
// Variables available on both server and client
|
||||
@@ -188,6 +204,20 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_GOOGLE_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_API_KEY,
|
||||
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: process.env.NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER,
|
||||
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
|
||||
NEXT_PUBLIC_BRAND_NAME: process.env.NEXT_PUBLIC_BRAND_NAME,
|
||||
NEXT_PUBLIC_BRAND_LOGO_URL: process.env.NEXT_PUBLIC_BRAND_LOGO_URL,
|
||||
NEXT_PUBLIC_BRAND_FAVICON_URL: process.env.NEXT_PUBLIC_BRAND_FAVICON_URL,
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: process.env.NEXT_PUBLIC_BRAND_PRIMARY_COLOR,
|
||||
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: process.env.NEXT_PUBLIC_BRAND_SECONDARY_COLOR,
|
||||
NEXT_PUBLIC_BRAND_ACCENT_COLOR: process.env.NEXT_PUBLIC_BRAND_ACCENT_COLOR,
|
||||
NEXT_PUBLIC_CUSTOM_CSS_URL: process.env.NEXT_PUBLIC_CUSTOM_CSS_URL,
|
||||
NEXT_PUBLIC_HIDE_BRANDING: process.env.NEXT_PUBLIC_HIDE_BRANDING,
|
||||
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: process.env.NEXT_PUBLIC_CUSTOM_FOOTER_TEXT,
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
|
||||
NEXT_PUBLIC_SUPPORT_URL: process.env.NEXT_PUBLIC_SUPPORT_URL,
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: process.env.NEXT_PUBLIC_DOCUMENTATION_URL,
|
||||
NEXT_PUBLIC_TERMS_URL: process.env.NEXT_PUBLIC_TERMS_URL,
|
||||
NEXT_PUBLIC_PRIVACY_URL: process.env.NEXT_PUBLIC_PRIVACY_URL,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED,
|
||||
},
|
||||
|
||||
15
bun.lock
15
bun.lock
@@ -15,12 +15,9 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0-beta.5",
|
||||
"@next/env": "^15.3.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"husky": "9.1.7",
|
||||
"lint-staged": "16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"turbo": "2.5.5",
|
||||
},
|
||||
},
|
||||
@@ -1260,8 +1257,6 @@
|
||||
|
||||
"@trigger.dev/sdk": ["@trigger.dev/sdk@3.3.17", "", { "dependencies": { "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.52.1", "@opentelemetry/semantic-conventions": "1.25.1", "@trigger.dev/core": "3.3.17", "chalk": "^5.2.0", "cronstrue": "^2.21.0", "debug": "^4.3.4", "evt": "^2.4.13", "slug": "^6.0.0", "terminal-link": "^3.0.0", "ulid": "^2.3.0", "uncrypto": "^0.1.3", "uuid": "^9.0.0", "ws": "^8.11.0" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-wjIjlQWKybYWw/J7LxFIOO1pXzxXoj9lxbFMvjb51JtfebxnQnh6aExN47nOGhVhV38wHYstfBI/8ClWwBnFYw=="],
|
||||
|
||||
"@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="],
|
||||
|
||||
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
@@ -2210,8 +2205,6 @@
|
||||
|
||||
"jaeger-client": ["jaeger-client@3.19.0", "", { "dependencies": { "node-int64": "^0.4.0", "opentracing": "^0.14.4", "thriftrw": "^3.5.0", "uuid": "^8.3.2", "xorshift": "^1.1.1" } }, "sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw=="],
|
||||
|
||||
"javascript-natural-sort": ["javascript-natural-sort@0.7.1", "", {}, "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="],
|
||||
|
||||
"jest-diff": ["jest-diff@26.6.2", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", "jest-get-type": "^26.3.0", "pretty-format": "^26.6.2" } }, "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA=="],
|
||||
|
||||
"jest-get-type": ["jest-get-type@26.3.0", "", {}, "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig=="],
|
||||
@@ -2670,9 +2663,7 @@
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
|
||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="],
|
||||
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
|
||||
|
||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||
|
||||
@@ -3474,8 +3465,6 @@
|
||||
|
||||
"@react-email/code-block/prismjs": ["prismjs@1.29.0", "", {}, "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="],
|
||||
|
||||
"@react-email/render/prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
|
||||
|
||||
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
@@ -4088,6 +4077,8 @@
|
||||
|
||||
"ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"resend/@react-email/render/prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
|
||||
"restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"sim/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
91
helm/sim/examples/values-whitelabeled.yaml
Normal file
91
helm/sim/examples/values-whitelabeled.yaml
Normal file
@@ -0,0 +1,91 @@
|
||||
# Whitelabeled deployment example for Sim
|
||||
# This configuration shows how to customize branding for Acme Corp
|
||||
|
||||
# Global configuration
|
||||
global:
|
||||
imageRegistry: "ghcr.io"
|
||||
storageClass: "managed-csi-premium"
|
||||
|
||||
# Main application with custom branding
|
||||
app:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
|
||||
# Custom branding configuration
|
||||
env:
|
||||
# Application URLs (update with your domain)
|
||||
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
|
||||
BETTER_AUTH_URL: "https://sim.acme.ai"
|
||||
SOCKET_SERVER_URL: "https://sim-ws.acme.ai"
|
||||
NEXT_PUBLIC_SOCKET_URL: "https://sim-ws.acme.ai"
|
||||
|
||||
# Security settings (REQUIRED)
|
||||
BETTER_AUTH_SECRET: "your-production-auth-secret-here"
|
||||
ENCRYPTION_KEY: "your-production-encryption-key-here"
|
||||
|
||||
# UI Branding & Whitelabeling Configuration
|
||||
NEXT_PUBLIC_BRAND_NAME: "Acme AI Studio"
|
||||
NEXT_PUBLIC_BRAND_LOGO_URL: "https://acme.com/assets/logo.png"
|
||||
NEXT_PUBLIC_BRAND_FAVICON_URL: "https://acme.com/assets/favicon.ico"
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: "#1a365d" # Acme blue
|
||||
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: "#2d3748" # Dark gray
|
||||
NEXT_PUBLIC_BRAND_ACCENT_COLOR: "#38b2ac" # Teal accent
|
||||
NEXT_PUBLIC_CUSTOM_CSS_URL: "https://acme.com/assets/theme.css"
|
||||
NEXT_PUBLIC_HIDE_BRANDING: "true" # Hide "Powered by Sim"
|
||||
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: "© 2024 Acme Corp. All rights reserved."
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: "ai-support@acme.com"
|
||||
NEXT_PUBLIC_SUPPORT_URL: "https://help.acme.com/ai-studio"
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: "https://docs.acme.com/ai-studio"
|
||||
NEXT_PUBLIC_TERMS_URL: "https://acme.com/terms"
|
||||
NEXT_PUBLIC_PRIVACY_URL: "https://acme.com/privacy"
|
||||
|
||||
# Realtime service
|
||||
realtime:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
env:
|
||||
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
|
||||
BETTER_AUTH_URL: "https://sim.acme.ai"
|
||||
NEXT_PUBLIC_SOCKET_URL: "https://sim-ws.acme.ai"
|
||||
BETTER_AUTH_SECRET: "your-production-auth-secret-here"
|
||||
ALLOWED_ORIGINS: "https://sim.acme.ai"
|
||||
|
||||
# PostgreSQL database
|
||||
postgresql:
|
||||
enabled: true
|
||||
auth:
|
||||
password: "your-secure-db-password-here"
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 20Gi
|
||||
|
||||
# Ingress configuration
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
|
||||
app:
|
||||
host: "sim.acme.ai"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
|
||||
realtime:
|
||||
host: "sim-ws.acme.ai"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: "sim-acme-tls"
|
||||
|
||||
# Auto-scaling
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
@@ -71,6 +71,22 @@ app:
|
||||
GITHUB_CLIENT_ID: ""
|
||||
GITHUB_CLIENT_SECRET: ""
|
||||
RESEND_API_KEY: ""
|
||||
|
||||
# UI Branding & Whitelabeling Configuration
|
||||
NEXT_PUBLIC_BRAND_NAME: "Sim" # Custom brand name
|
||||
NEXT_PUBLIC_BRAND_LOGO_URL: "" # Custom logo URL (leave empty for default)
|
||||
NEXT_PUBLIC_BRAND_FAVICON_URL: "" # Custom favicon URL (leave empty for default)
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: "#000000" # Primary brand color (hex)
|
||||
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: "#6366f1" # Secondary brand color (hex)
|
||||
NEXT_PUBLIC_BRAND_ACCENT_COLOR: "#f59e0b" # Accent brand color (hex)
|
||||
NEXT_PUBLIC_CUSTOM_CSS_URL: "" # Custom stylesheet URL (leave empty for none)
|
||||
NEXT_PUBLIC_HIDE_BRANDING: "false" # Hide "Powered by" branding (true/false)
|
||||
NEXT_PUBLIC_CUSTOM_FOOTER_TEXT: "" # Custom footer text (leave empty for default)
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: "help@sim.ai" # Support email address
|
||||
NEXT_PUBLIC_SUPPORT_URL: "" # Support page URL (leave empty for none)
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: "" # Documentation URL (leave empty for none)
|
||||
NEXT_PUBLIC_TERMS_URL: "" # Terms of service URL (leave empty for none)
|
||||
NEXT_PUBLIC_PRIVACY_URL: "" # Privacy policy URL (leave empty for none)
|
||||
|
||||
# Service configuration
|
||||
service:
|
||||
|
||||
@@ -41,12 +41,9 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0-beta.5",
|
||||
"@next/env": "^15.3.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"husky": "9.1.7",
|
||||
"lint-staged": "16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"turbo": "2.5.5"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
Reference in New Issue
Block a user