v0.3.27: oauth/webhook fixes, whitelabel fixes, code cleanups

v0.3.27: oauth/webhook fixes, whitelabel fixes, code cleanups
This commit is contained in:
Vikhyath Mondreti
2025-08-15 13:33:55 -07:00
committed by GitHub
183 changed files with 15367 additions and 1750 deletions

View File

@@ -9,7 +9,7 @@ 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'>
<main className='relative flex min-h-screen flex-col bg-[var(--brand-background-hex)] font-geist-sans text-white'>
{/* Background pattern */}
<GridPattern
x={-5}

View File

@@ -456,7 +456,7 @@ export default function LoginPage({
<Button
type='submit'
className='flex h-11 w-full items-center justify-center gap-2 bg-[#701ffc] font-medium text-base text-white shadow-[#701ffc]/20 shadow-lg transition-colors duration-200 hover:bg-[#802FFF]'
className='flex h-11 w-full items-center justify-center gap-2 bg-brand-primary font-medium text-base text-white shadow-[var(--brand-primary-hex)]/20 shadow-lg transition-colors duration-200 hover:bg-brand-primary-hover'
disabled={isLoading}
>
{isLoading ? 'Signing in...' : 'Sign In'}
@@ -468,7 +468,7 @@ export default function LoginPage({
<span className='text-neutral-400'>Don't have an account? </span>
<Link
href={isInviteFlow ? `/signup?invite_flow=true&callbackUrl=${callbackUrl}` : '/signup'}
className='font-medium text-[#9D54FF] underline-offset-4 transition hover:text-[#a66fff] hover:underline'
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
>
Sign up
</Link>
@@ -497,7 +497,7 @@ export default function LoginPage({
placeholder='Enter your email'
required
type='email'
className='border-neutral-700/80 bg-neutral-900 text-white placeholder:text-white/60 focus:border-[#802FFF]/70 focus:ring-[#802FFF]/20'
className='border-neutral-700/80 bg-neutral-900 text-white placeholder:text-white/60 focus:border-[var(--brand-primary-hover-hex)]/70 focus:ring-[var(--brand-primary-hover-hex)]/20'
/>
</div>
{resetStatus.type && (
@@ -512,7 +512,7 @@ export default function LoginPage({
<Button
type='button'
onClick={handleForgotPassword}
className='h-11 w-full bg-[#701ffc] font-medium text-base text-white shadow-[#701ffc]/20 shadow-lg transition-colors duration-200 hover:bg-[#802FFF]'
className='h-11 w-full bg-[var(--brand-primary-hex)] font-medium text-base text-white shadow-[var(--brand-primary-hex)]/20 shadow-lg transition-colors duration-200 hover:bg-[var(--brand-primary-hover-hex)]'
disabled={isSubmittingReset}
>
{isSubmittingReset ? 'Sending...' : 'Send Reset Link'}

View File

@@ -488,7 +488,7 @@ function SignupFormContent({
<Button
type='submit'
className='flex h-11 w-full items-center justify-center gap-2 bg-[#701ffc] font-medium text-base text-white shadow-[#701ffc]/20 shadow-lg transition-colors duration-200 hover:bg-[#802FFF]'
className='flex h-11 w-full items-center justify-center gap-2 bg-[var(--brand-primary-hex)] font-medium text-base text-white shadow-[var(--brand-primary-hex)]/20 shadow-lg transition-colors duration-200 hover:bg-[var(--brand-primary-hover-hex)]'
disabled={isLoading}
>
{isLoading ? 'Creating account...' : 'Create Account'}
@@ -500,7 +500,7 @@ function SignupFormContent({
<span className='text-neutral-400'>Already have an account? </span>
<Link
href={isInviteFlow ? `/login?invite_flow=true&callbackUrl=${redirectUrl}` : '/login'}
className='font-medium text-[#9D54FF] underline-offset-4 transition hover:text-[#a66fff] hover:underline'
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
>
Sign in
</Link>

View File

@@ -124,7 +124,7 @@ function VerificationForm({
<Button
onClick={verifyCode}
className='h-11 w-full bg-[#701ffc] font-medium text-base text-white shadow-[#701ffc]/20 shadow-lg transition-colors duration-200 hover:bg-[#802FFF]'
className='h-11 w-full bg-[var(--brand-primary-hex)] font-medium text-base text-white shadow-[var(--brand-primary-hex)]/20 shadow-lg transition-colors duration-200 hover:bg-[var(--brand-primary-hover-hex)]'
disabled={!isOtpComplete || isLoading}
>
{isLoading ? 'Verifying...' : 'Verify Email'}
@@ -140,7 +140,7 @@ function VerificationForm({
</span>
) : (
<button
className='font-medium text-[#9D54FF] underline-offset-4 transition hover:text-[#a66fff] hover:underline'
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
onClick={handleResend}
disabled={isLoading || isResendDisabled}
>

View File

@@ -35,7 +35,7 @@ export const BlogCard = ({
}: BlogCardProps) => {
return (
<Link href={href}>
<div className='flex flex-col rounded-3xl border border-[#606060]/40 bg-[#101010] p-8 transition-all duration-500 hover:bg-[#202020]'>
<div className='flex flex-col rounded-3xl border border-[#606060]/40 bg-[#101010] p-8 transition-all duration-500 hover:bg-[var(--surface-elevated)]'>
{image ? (
<Image
src={image}

View File

@@ -245,7 +245,7 @@ export default function NavClient({
target='_blank'
rel='noopener noreferrer'
>
<Button className='h-[43px] bg-[#701ffc] px-6 py-2 font-geist-sans font-medium text-base text-neutral-100 transition-colors duration-200 hover:bg-[#802FFF]'>
<Button className='h-[43px] bg-[var(--brand-primary-hex)] px-6 py-2 font-geist-sans font-medium text-base text-neutral-100 transition-colors duration-200 hover:bg-[var(--brand-primary-hover-hex)]'>
Contact
</Button>
</Link>
@@ -277,7 +277,7 @@ export default function NavClient({
>
<SheetContent
side='right'
className='flex h-full w-[280px] flex-col border-[#181818] border-l bg-[#0C0C0C] p-6 pt-6 text-white shadow-xl sm:w-[320px] [&>button]:hidden'
className='flex h-full w-[280px] flex-col border-[#181818] border-l bg-[var(--brand-background-hex)] p-6 pt-6 text-white shadow-xl sm:w-[320px] [&>button]:hidden'
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
>
@@ -311,7 +311,7 @@ export default function NavClient({
target='_blank'
rel='noopener noreferrer'
>
<Button className='w-full bg-[#701ffc] py-6 font-medium text-base text-white shadow-[#701ffc]/20 shadow-lg transition-colors duration-200 hover:bg-[#802FFF]'>
<Button className='w-full bg-[var(--brand-primary-hex)] py-6 font-medium text-base text-white shadow-[var(--brand-primary-hex)]/20 shadow-lg transition-colors duration-200 hover:bg-[var(--brand-primary-hover-hex)]'>
Contact
</Button>
</Link>

View File

@@ -62,7 +62,7 @@ function Hero() {
<Button
variant={'secondary'}
onClick={handleNavigate}
className='animate-fade-in items-center bg-[#701ffc] px-7 py-6 font-[420] font-geist-sans text-lg text-neutral-100 tracking-normal shadow-[#701ffc]/30 shadow-lg hover:bg-[#802FFF]'
className='animate-fade-in items-center bg-[var(--brand-primary-hex)] px-7 py-6 font-[420] font-geist-sans text-lg text-neutral-100 tracking-normal shadow-[var(--brand-primary-hex)]/30 shadow-lg hover:bg-[var(--brand-primary-hover-hex)]'
aria-label='Start using the platform'
>
<div className='text-[1.15rem]'>Start now</div>
@@ -104,7 +104,7 @@ function Hero() {
className='aspect-[5/3] h-auto md:aspect-auto'
>
<g filter='url(#filter0_b_0_1)'>
<ellipse cx='300' cy='240' rx='290' ry='220' fill='#0C0C0C' />
<ellipse cx='300' cy='240' rx='290' ry='220' fill='var(--brand-background-hex)' />
</g>
<defs>
<filter

View File

@@ -151,7 +151,7 @@ export default function ContributorsPage() {
)
return (
<main className='relative min-h-screen bg-[#0C0C0C] font-geist-sans text-white'>
<main className='relative min-h-screen bg-[var(--brand-background-hex)] font-geist-sans text-white'>
{/* Grid pattern background */}
<div className='absolute inset-0 bottom-[400px] z-0'>
<GridPattern
@@ -239,7 +239,7 @@ export default function ContributorsPage() {
<div className='mb-6 grid grid-cols-1 gap-3 sm:mb-8 sm:grid-cols-2 sm:gap-4 lg:grid-cols-5'>
<div className='rounded-lg border border-[#606060]/20 bg-neutral-800/30 p-3 text-center sm:rounded-xl sm:p-4'>
<div className='mb-1 flex items-center justify-center sm:mb-2'>
<Star className='h-4 w-4 text-[#701ffc] sm:h-5 sm:w-5' />
<Star className='h-4 w-4 text-[var(--brand-primary-hex)] sm:h-5 sm:w-5' />
</div>
<div className='font-bold text-lg text-white sm:text-xl'>{repoStats.stars}</div>
<div className='text-neutral-400 text-xs'>Stars</div>
@@ -247,7 +247,7 @@ export default function ContributorsPage() {
<div className='rounded-lg border border-[#606060]/20 bg-neutral-800/30 p-3 text-center sm:rounded-xl sm:p-4'>
<div className='mb-1 flex items-center justify-center sm:mb-2'>
<GitFork className='h-4 w-4 text-[#701ffc] sm:h-5 sm:w-5' />
<GitFork className='h-4 w-4 text-[var(--brand-primary-hex)] sm:h-5 sm:w-5' />
</div>
<div className='font-bold text-lg text-white sm:text-xl'>{repoStats.forks}</div>
<div className='text-neutral-400 text-xs'>Forks</div>
@@ -255,7 +255,7 @@ export default function ContributorsPage() {
<div className='rounded-lg border border-[#606060]/20 bg-neutral-800/30 p-3 text-center sm:rounded-xl sm:p-4'>
<div className='mb-1 flex items-center justify-center sm:mb-2'>
<GitGraph className='h-4 w-4 text-[#701ffc] sm:h-5 sm:w-5' />
<GitGraph className='h-4 w-4 text-[var(--brand-primary-hex)] sm:h-5 sm:w-5' />
</div>
<div className='font-bold text-lg text-white sm:text-xl'>
{filteredContributors?.length || 0}
@@ -265,7 +265,7 @@ export default function ContributorsPage() {
<div className='rounded-lg border border-[#606060]/20 bg-neutral-800/30 p-3 text-center sm:rounded-xl sm:p-4'>
<div className='mb-1 flex items-center justify-center sm:mb-2'>
<MessageCircle className='h-4 w-4 text-[#701ffc] sm:h-5 sm:w-5' />
<MessageCircle className='h-4 w-4 text-[var(--brand-primary-hex)] sm:h-5 sm:w-5' />
</div>
<div className='font-bold text-lg text-white sm:text-xl'>
{repoStats.openIssues}
@@ -275,7 +275,7 @@ export default function ContributorsPage() {
<div className='rounded-lg border border-[#606060]/20 bg-neutral-800/30 p-3 text-center sm:rounded-xl sm:p-4'>
<div className='mb-1 flex items-center justify-center sm:mb-2'>
<GitPullRequest className='h-4 w-4 text-[#701ffc] sm:h-5 sm:w-5' />
<GitPullRequest className='h-4 w-4 text-[var(--brand-primary-hex)] sm:h-5 sm:w-5' />
</div>
<div className='font-bold text-lg text-white sm:text-xl'>{repoStats.openPRs}</div>
<div className='text-neutral-400 text-xs'>Pull Requests</div>
@@ -291,8 +291,8 @@ export default function ContributorsPage() {
<AreaChart data={timelineData} className='-mx-2 sm:-mx-5 mt-1 sm:mt-2'>
<defs>
<linearGradient id='commits' x1='0' y1='0' x2='0' y2='1'>
<stop offset='5%' stopColor='#701ffc' stopOpacity={0.3} />
<stop offset='95%' stopColor='#701ffc' stopOpacity={0} />
<stop offset='5%' stopColor='var(--brand-primary-hex)' stopOpacity={0.3} />
<stop offset='95%' stopColor='var(--brand-primary-hex)' stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
@@ -320,7 +320,7 @@ export default function ContributorsPage() {
<div className='rounded-lg border border-[#606060]/30 bg-[#0f0f0f] p-2 shadow-lg backdrop-blur-sm sm:p-3'>
<div className='grid gap-1 sm:gap-2'>
<div className='flex items-center gap-1 sm:gap-2'>
<GitGraph className='h-3 w-3 text-[#701ffc] sm:h-4 sm:w-4' />
<GitGraph className='h-3 w-3 text-[var(--brand-primary-hex)] sm:h-4 sm:w-4' />
<span className='text-neutral-400 text-xs sm:text-sm'>
Commits:
</span>
@@ -338,7 +338,7 @@ export default function ContributorsPage() {
<Area
type='monotone'
dataKey='commits'
stroke='#701ffc'
stroke='var(--brand-primary-hex)'
strokeWidth={2}
fill='url(#commits)'
/>
@@ -393,7 +393,7 @@ export default function ContributorsPage() {
animate={{ opacity: 1, y: 0 }}
style={{ animationDelay: `${index * 50}ms` }}
>
<Avatar className='h-12 w-12 ring-2 ring-[#606060]/30 transition-transform group-hover:scale-105 group-hover:ring-[#701ffc]/60 sm:h-16 sm:w-16'>
<Avatar className='h-12 w-12 ring-2 ring-[#606060]/30 transition-transform group-hover:scale-105 group-hover:ring-[var(--brand-primary-hex)]/60 sm:h-16 sm:w-16'>
<AvatarImage
src={contributor.avatar_url}
alt={contributor.login}
@@ -405,13 +405,13 @@ export default function ContributorsPage() {
</Avatar>
<div className='mt-2 text-center sm:mt-3'>
<span className='block font-medium text-white text-xs transition-colors group-hover:text-[#701ffc] sm:text-sm'>
<span className='block font-medium text-white text-xs transition-colors group-hover:text-[var(--brand-primary-hex)] sm:text-sm'>
{contributor.login.length > 12
? `${contributor.login.slice(0, 12)}...`
: contributor.login}
</span>
<div className='mt-1 flex items-center justify-center gap-1 sm:mt-2'>
<GitGraph className='h-2 w-2 text-neutral-400 transition-colors group-hover:text-[#701ffc] sm:h-3 sm:w-3' />
<GitGraph className='h-2 w-2 text-neutral-400 transition-colors group-hover:text-[var(--brand-primary-hex)] sm:h-3 sm:w-3' />
<span className='font-medium text-neutral-300 text-xs transition-colors group-hover:text-white sm:text-sm'>
{contributor.contributions}
</span>
@@ -508,7 +508,7 @@ export default function ContributorsPage() {
/>
<Bar
dataKey='contributions'
className='fill-[#701ffc]'
className='fill-[var(--brand-primary-hex)]'
radius={[4, 4, 0, 0]}
/>
</BarChart>
@@ -532,7 +532,7 @@ export default function ContributorsPage() {
>
<div className='relative p-6 sm:p-8 md:p-12 lg:p-16'>
<div className='text-center'>
<div className='mb-4 inline-flex items-center rounded-full border border-[#701ffc]/20 bg-[#701ffc]/10 px-3 py-1 font-medium text-[#701ffc] text-xs sm:mb-6 sm:px-4 sm:py-2 sm:text-sm'>
<div className='mb-4 inline-flex items-center rounded-full border border-[var(--brand-primary-hex)]/20 bg-[var(--brand-primary-hex)]/10 px-3 py-1 font-medium text-[var(--brand-primary-hex)] text-xs sm:mb-6 sm:px-4 sm:py-2 sm:text-sm'>
<Github className='mr-1 h-3 w-3 sm:mr-2 sm:h-4 sm:w-4' />
Apache-2.0 Licensed
</div>
@@ -550,7 +550,7 @@ export default function ContributorsPage() {
<Button
asChild
size='lg'
className='bg-[#701ffc] text-white transition-colors duration-500 hover:bg-[#802FFF]'
className='bg-[var(--brand-primary-hex)] text-white transition-colors duration-500 hover:bg-[var(--brand-primary-hover-hex)]'
>
<a
href='https://github.com/simstudioai/sim/blob/main/.github/CONTRIBUTING.md'

View File

@@ -12,7 +12,7 @@ export default function Landing() {
}
return (
<main className='relative min-h-screen bg-[#0C0C0C] font-geist-sans'>
<main className='relative min-h-screen bg-[var(--brand-background-hex)] font-geist-sans'>
<NavWrapper onOpenTypeformLink={handleOpenTypeformLink} />
<Hero />

View File

@@ -11,7 +11,7 @@ export default function PrivacyPolicy() {
}
return (
<main className='relative min-h-screen overflow-hidden bg-[#0C0C0C] text-white'>
<main className='relative min-h-screen overflow-hidden bg-[var(--brand-background-hex)] text-white'>
{/* Grid pattern background - only covers content area */}
<div className='absolute inset-0 bottom-[400px] z-0 overflow-hidden'>
<GridPattern
@@ -42,7 +42,7 @@ export default function PrivacyPolicy() {
className='h-full w-full'
>
<g filter='url(#filter0_b_privacy)'>
<rect width='600' height='1600' rx='0' fill='#0C0C0C' />
<rect width='600' height='1600' rx='0' fill='var(--brand-background-hex)' />
</g>
<defs>
<filter
@@ -391,7 +391,7 @@ export default function PrivacyPolicy() {
Privacy & Terms web page:{' '}
<Link
href='https://policies.google.com/privacy?hl=en'
className='text-[#B5A1D4] hover:text-[#701ffc]'
className='text-[#B5A1D4] hover:text-[var(--brand-primary-hex)]'
target='_blank'
rel='noopener noreferrer'
>
@@ -569,7 +569,7 @@ export default function PrivacyPolicy() {
Please note that we may ask you to verify your identity before responding to such
requests.
</p>
<p className='mb-4 border-[#701ffc] border-l-4 bg-[#701ffc]/10 p-3'>
<p className='mb-4 border-[var(--brand-primary-hex)] border-l-4 bg-[var(--brand-primary-hex)]/10 p-3'>
You have the right to complain to a Data Protection Authority about our collection
and use of your Personal Information. For more information, please contact your
local data protection authority in the European Economic Area (EEA).
@@ -661,7 +661,7 @@ export default function PrivacyPolicy() {
policy (if any). Before beginning your inquiry, email us at{' '}
<Link
href='mailto:security@sim.ai'
className='text-[#B5A1D4] hover:text-[#701ffc]'
className='text-[#B5A1D4] hover:text-[var(--brand-primary-hex)]'
>
security@sim.ai
</Link>{' '}
@@ -686,7 +686,7 @@ export default function PrivacyPolicy() {
To report any security flaws, send an email to{' '}
<Link
href='mailto:security@sim.ai'
className='text-[#B5A1D4] hover:text-[#701ffc]'
className='text-[#B5A1D4] hover:text-[var(--brand-primary-hex)]'
>
security@sim.ai
</Link>
@@ -726,7 +726,7 @@ export default function PrivacyPolicy() {
If you have any questions about this Privacy Policy, please contact us at:{' '}
<Link
href='mailto:privacy@sim.ai'
className='text-[#B5A1D4] hover:text-[#701ffc]'
className='text-[#B5A1D4] hover:text-[var(--brand-primary-hex)]'
>
privacy@sim.ai
</Link>

View File

@@ -11,7 +11,7 @@ export default function TermsOfService() {
}
return (
<main className='relative min-h-screen overflow-hidden bg-[#0C0C0C] text-white'>
<main className='relative min-h-screen overflow-hidden bg-[var(--brand-background-hex)] text-white'>
{/* Grid pattern background */}
<div className='absolute inset-0 bottom-[400px] z-0 overflow-hidden'>
<GridPattern
@@ -42,7 +42,7 @@ export default function TermsOfService() {
className='h-full w-full'
>
<g filter='url(#filter0_b_terms)'>
<rect width='600' height='1600' rx='0' fill='#0C0C0C' />
<rect width='600' height='1600' rx='0' fill='var(--brand-background-hex)' />
</g>
<defs>
<filter
@@ -268,7 +268,7 @@ export default function TermsOfService() {
Arbitration Agreement. The arbitration will be conducted by JAMS, an established
alternative dispute resolution provider.
</p>
<p className='mb-4 border-[#701ffc] border-l-4 bg-[#701ffc]/10 p-3'>
<p className='mb-4 border-[var(--brand-primary-hex)] border-l-4 bg-[var(--brand-primary-hex)]/10 p-3'>
YOU AND COMPANY AGREE THAT EACH OF US MAY BRING CLAIMS AGAINST THE OTHER ONLY ON
AN INDIVIDUAL BASIS AND NOT ON A CLASS, REPRESENTATIVE, OR COLLECTIVE BASIS. ONLY
INDIVIDUAL RELIEF IS AVAILABLE, AND DISPUTES OF MORE THAN ONE CUSTOMER OR USER
@@ -277,7 +277,10 @@ export default function TermsOfService() {
<p className='mb-4'>
You have the right to opt out of the provisions of this Arbitration Agreement by
sending a timely written notice of your decision to opt out to:{' '}
<Link href='mailto:legal@sim.ai' className='text-[#B5A1D4] hover:text-[#701ffc]'>
<Link
href='mailto:legal@sim.ai'
className='text-[#B5A1D4] hover:text-[var(--brand-primary-hex)]'
>
legal@sim.ai{' '}
</Link>
within 30 days after first becoming subject to this Arbitration Agreement.
@@ -330,7 +333,7 @@ export default function TermsOfService() {
Our Copyright Agent can be reached at:{' '}
<Link
href='mailto:copyright@sim.ai'
className='text-[#B5A1D4] hover:text-[#701ffc]'
className='text-[#B5A1D4] hover:text-[var(--brand-primary-hex)]'
>
copyright@sim.ai
</Link>
@@ -341,7 +344,10 @@ export default function TermsOfService() {
<h2 className='mb-4 font-semibold text-2xl text-white'>12. Contact Us</h2>
<p>
If you have any questions about these Terms, please contact us at:{' '}
<Link href='mailto:legal@sim.ai' className='text-[#B5A1D4] hover:text-[#701ffc]'>
<Link
href='mailto:legal@sim.ai'
className='text-[#B5A1D4] hover:text-[var(--brand-primary-hex)]'
>
legal@sim.ai
</Link>
</p>

View File

@@ -28,7 +28,12 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request, { credentialId, workflowId })
// We already have workflowId from the parsed body; avoid forcing hybrid auth to re-read it
const authz = await authorizeCredentialUse(request, {
credentialId,
workflowId,
requireWorkflowIdForInternal: false,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

View File

@@ -2,8 +2,9 @@ import crypto from 'crypto'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalApiKey } from '@/lib/copilot/utils'
import { env } from '@/lib/env'
import { isProd } from '@/lib/environment'
import { isBillingEnabled, isProd } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { db } from '@/db'
import { userStats } from '@/db/schema'
@@ -11,7 +12,6 @@ import { calculateCost } from '@/providers/utils'
const logger = createLogger('billing-update-cost')
// Schema for the request body
const UpdateCostSchema = z.object({
userId: z.string().min(1, 'User ID is required'),
input: z.number().min(0, 'Input tokens must be a non-negative number'),
@@ -19,26 +19,6 @@ const UpdateCostSchema = z.object({
model: z.string().min(1, 'Model is required'),
})
// Authentication function (reused from copilot/methods route)
function checkInternalApiKey(req: NextRequest) {
const apiKey = req.headers.get('x-api-key')
const expectedApiKey = env.INTERNAL_API_SECRET
if (!expectedApiKey) {
return { success: false, error: 'Internal API key not configured' }
}
if (!apiKey) {
return { success: false, error: 'API key required' }
}
if (apiKey !== expectedApiKey) {
return { success: false, error: 'Invalid API key' }
}
return { success: true }
}
/**
* POST /api/billing/update-cost
* Update user cost based on token usage with internal API key auth
@@ -50,6 +30,19 @@ export async function POST(req: NextRequest) {
try {
logger.info(`[${requestId}] Update cost request started`)
if (!isBillingEnabled) {
logger.debug(`[${requestId}] Billing is disabled, skipping cost update`)
return NextResponse.json({
success: true,
message: 'Billing disabled, cost update skipped',
data: {
billingEnabled: false,
processedAt: new Date().toISOString(),
requestId,
},
})
}
// Check authentication (internal API key)
const authResult = checkInternalApiKey(req)
if (!authResult.success) {

View File

@@ -246,7 +246,10 @@ describe('Chat API Route', () => {
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
},
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
typeof value === 'string'
? value.toLowerCase() === 'true' || value === '1'
: Boolean(value),
getEnv: (variable: string) => process.env[variable],
}))
const validData = {
@@ -291,6 +294,7 @@ describe('Chat API Route', () => {
},
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
getEnv: (variable: string) => process.env[variable],
}))
const validData = {

View File

@@ -14,8 +14,6 @@ import { chat } from '@/db/schema'
const logger = createLogger('ChatAPI')
export const dynamic = 'force-dynamic'
const chatSchema = z.object({
workflowId: z.string().min(1, 'Workflow ID is required'),
subdomain: z
@@ -150,7 +148,7 @@ export async function POST(request: NextRequest) {
// Merge customizations with the additional fields
const mergedCustomizations = {
...(customizations || {}),
primaryColor: customizations?.primaryColor || '#802FFF',
primaryColor: customizations?.primaryColor || 'var(--brand-primary-hover-hex)',
welcomeMessage: customizations?.welcomeMessage || 'Hi there! How can I help you today?',
}

View File

@@ -2,9 +2,6 @@ import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
import { chat } from '@/db/schema'

View File

@@ -223,6 +223,8 @@ describe('Copilot Chat API Route', () => {
stream: true,
streamToolCalls: true,
mode: 'agent',
provider: 'openai',
depth: 0,
}),
})
)
@@ -284,6 +286,8 @@ describe('Copilot Chat API Route', () => {
stream: true,
streamToolCalls: true,
mode: 'agent',
provider: 'openai',
depth: 0,
}),
})
)
@@ -337,6 +341,8 @@ describe('Copilot Chat API Route', () => {
stream: true,
streamToolCalls: true,
mode: 'agent',
provider: 'openai',
depth: 0,
}),
})
)
@@ -430,6 +436,8 @@ describe('Copilot Chat API Route', () => {
stream: true,
streamToolCalls: true,
mode: 'ask',
provider: 'openai',
depth: 0,
}),
})
)

View File

@@ -39,10 +39,13 @@ const ChatMessageSchema = z.object({
chatId: z.string().optional(),
workflowId: z.string().min(1, 'Workflow ID is required'),
mode: z.enum(['ask', 'agent']).optional().default('agent'),
depth: z.number().int().min(0).max(3).optional().default(0),
createNewChat: z.boolean().optional().default(false),
stream: z.boolean().optional().default(true),
implicitFeedback: z.string().optional(),
fileAttachments: z.array(FileAttachmentSchema).optional(),
provider: z.string().optional().default('openai'),
conversationId: z.string().optional(),
})
// Sim Agent API configuration
@@ -156,10 +159,13 @@ export async function POST(req: NextRequest) {
chatId,
workflowId,
mode,
depth,
createNewChat,
stream,
implicitFeedback,
fileAttachments,
provider,
conversationId,
} = ChatMessageSchema.parse(body)
logger.info(`[${tracker.requestId}] Processing copilot chat request`, {
@@ -171,6 +177,8 @@ export async function POST(req: NextRequest) {
createNewChat,
messageLength: message.length,
hasImplicitFeedback: !!implicitFeedback,
provider: provider || 'openai',
hasConversationId: !!conversationId,
})
// Handle chat context
@@ -252,7 +260,7 @@ export async function POST(req: NextRequest) {
}
// Build messages array for sim agent with conversation history
const messages = []
const messages: any[] = []
// Add conversation history (need to rebuild these with file support if they had attachments)
for (const msg of conversationHistory) {
@@ -327,16 +335,13 @@ export async function POST(req: NextRequest) {
})
}
// Start title generation in parallel if this is a new chat with first message
if (actualChatId && !currentChat?.title && conversationHistory.length === 0) {
logger.info(`[${tracker.requestId}] Will start parallel title generation inside stream`)
}
// Determine provider and conversationId to use for this request
const providerToUse = provider || 'openai'
const effectiveConversationId =
(currentChat?.conversationId as string | undefined) || conversationId
// Forward to sim agent API
logger.info(`[${tracker.requestId}] Sending request to sim agent API`, {
messageCount: messages.length,
endpoint: `${SIM_AGENT_API_URL}/api/chat-completion-streaming`,
})
// If we have a conversationId, only send the most recent user message; else send full history
const messagesForAgent = effectiveConversationId ? [messages[messages.length - 1]] : messages
const simAgentResponse = await fetch(`${SIM_AGENT_API_URL}/api/chat-completion-streaming`, {
method: 'POST',
@@ -345,12 +350,15 @@ export async function POST(req: NextRequest) {
...(SIM_AGENT_API_KEY && { 'x-api-key': SIM_AGENT_API_KEY }),
},
body: JSON.stringify({
messages,
messages: messagesForAgent,
workflowId,
userId: authenticatedUserId,
stream: stream,
streamToolCalls: true,
mode: mode,
provider: providerToUse,
...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}),
...(typeof depth === 'number' ? { depth } : {}),
...(session?.user?.name && { userName: session.user.name }),
}),
})
@@ -388,6 +396,8 @@ export async function POST(req: NextRequest) {
const toolCalls: any[] = []
let buffer = ''
let isFirstDone = true
let responseIdFromStart: string | undefined
let responseIdFromDone: string | undefined
// Send chatId as first event
if (actualChatId) {
@@ -486,6 +496,13 @@ export async function POST(req: NextRequest) {
}
break
case 'reasoning':
// Treat like thinking: do not add to assistantContent to avoid leaking
logger.debug(
`[${tracker.requestId}] Reasoning chunk received (${(event.data || event.content || '').length} chars)`
)
break
case 'tool_call':
logger.info(
`[${tracker.requestId}] Tool call ${event.data?.partial ? '(partial)' : '(complete)'}:`,
@@ -528,7 +545,22 @@ export async function POST(req: NextRequest) {
})
break
case 'start':
if (event.data?.responseId) {
responseIdFromStart = event.data.responseId
logger.info(
`[${tracker.requestId}] Received start event with responseId: ${responseIdFromStart}`
)
}
break
case 'done':
if (event.data?.responseId) {
responseIdFromDone = event.data.responseId
logger.info(
`[${tracker.requestId}] Received done event with responseId: ${responseIdFromDone}`
)
}
if (isFirstDone) {
logger.info(
`[${tracker.requestId}] Initial AI response complete, tool count: ${toolCalls.length}`
@@ -622,12 +654,15 @@ export async function POST(req: NextRequest) {
)
}
const responseId = responseIdFromDone || responseIdFromStart
// Update chat in database immediately (without title)
await db
.update(copilotChats)
.set({
messages: updatedMessages,
updatedAt: new Date(),
...(responseId ? { conversationId: responseId } : {}),
})
.where(eq(copilotChats.id, actualChatId!))
@@ -635,6 +670,7 @@ export async function POST(req: NextRequest) {
messageCount: updatedMessages.length,
savedUserMessage: true,
savedAssistantMessage: assistantContent.trim().length > 0,
updatedConversationId: responseId || null,
})
}
} catch (error) {

View File

@@ -51,12 +51,6 @@ export async function POST(req: NextRequest) {
const body = await req.json()
const { chatId, messages } = UpdateMessagesSchema.parse(body)
logger.info(`[${tracker.requestId}] Updating chat messages`, {
userId,
chatId,
messageCount: messages.length,
})
// Verify that the chat belongs to the user
const [chat] = await db
.select()

View File

@@ -38,7 +38,7 @@ async function updateToolCallStatus(
try {
const key = `tool_call:${toolCallId}`
const timeout = 60000 // 1 minute timeout
const timeout = 600000 // 10 minutes timeout for user confirmation
const pollInterval = 100 // Poll every 100ms
const startTime = Date.now()

View File

@@ -2,7 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { copilotToolRegistry } from '@/lib/copilot/tools/server-tools/registry'
import type { NotificationStatus } from '@/lib/copilot/types'
import { env } from '@/lib/env'
import { checkInternalApiKey } from '@/lib/copilot/utils'
import { createLogger } from '@/lib/logs/console/logger'
import { getRedisClient } from '@/lib/redis'
import { createErrorResponse } from '@/app/api/copilot/methods/utils'
@@ -65,7 +65,7 @@ async function pollRedisForTool(
}
const key = `tool_call:${toolCallId}`
const timeout = 300000 // 5 minutes
const timeout = 600000 // 10 minutes for long-running operations
const pollInterval = 1000 // 1 second
const startTime = Date.now()
@@ -240,33 +240,12 @@ async function interruptHandler(toolCallId: string): Promise<{
}
}
// Schema for method execution
const MethodExecutionSchema = z.object({
methodId: z.string().min(1, 'Method ID is required'),
params: z.record(z.any()).optional().default({}),
toolCallId: z.string().nullable().optional().default(null),
})
// Simple internal API key authentication
function checkInternalApiKey(req: NextRequest) {
const apiKey = req.headers.get('x-api-key')
const expectedApiKey = env.INTERNAL_API_SECRET
if (!expectedApiKey) {
return { success: false, error: 'Internal API key not configured' }
}
if (!apiKey) {
return { success: false, error: 'API key required' }
}
if (apiKey !== expectedApiKey) {
return { success: false, error: 'Invalid API key' }
}
return { success: true }
}
/**
* POST /api/copilot/methods
* Execute a method based on methodId with internal API key auth

View File

@@ -3,9 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { decryptSecret, encryptSecret } from '@/lib/utils'
import { db } from '@/db'
import { environment } from '@/db/schema'

View File

@@ -7,7 +7,6 @@ import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
const mockFreestyleExecuteScript = vi.fn()
const mockCreateContext = vi.fn()
const mockRunInContext = vi.fn()
const mockLogger = {
@@ -29,27 +28,10 @@ describe('Function Execute API Route', () => {
})),
}))
vi.doMock('freestyle-sandboxes', () => ({
FreestyleSandboxes: vi.fn().mockImplementation(() => ({
executeScript: mockFreestyleExecuteScript,
})),
}))
vi.doMock('@/lib/env', () => ({
env: {
FREESTYLE_API_KEY: 'test-freestyle-key',
},
}))
vi.doMock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
mockFreestyleExecuteScript.mockResolvedValue({
result: 'freestyle success',
logs: [],
})
mockRunInContext.mockResolvedValue('vm success')
mockCreateContext.mockReturnValue({})
})
@@ -228,107 +210,6 @@ describe('Function Execute API Route', () => {
})
})
describe.skip('Freestyle Execution', () => {
it('should use Freestyle when API key is available', async () => {
const req = createMockRequest('POST', {
code: 'return "freestyle test"',
})
const { POST } = await import('@/app/api/function/execute/route')
await POST(req)
expect(mockFreestyleExecuteScript).toHaveBeenCalled()
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringMatching(/\[.*\] Using Freestyle for code execution/)
)
})
it('should handle Freestyle errors and fallback to VM', async () => {
mockFreestyleExecuteScript.mockRejectedValueOnce(new Error('Freestyle API error'))
const req = createMockRequest('POST', {
code: 'return "fallback test"',
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(mockFreestyleExecuteScript).toHaveBeenCalled()
expect(mockRunInContext).toHaveBeenCalled()
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringMatching(/\[.*\] Freestyle API call failed, falling back to VM:/),
expect.any(Object)
)
})
it('should handle Freestyle script errors', async () => {
mockFreestyleExecuteScript.mockResolvedValueOnce({
result: null,
logs: [{ type: 'error', message: 'ReferenceError: undefined variable' }],
})
const req = createMockRequest('POST', {
code: 'return undefinedVariable',
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(500)
const data = await response.json()
expect(data.success).toBe(false)
})
})
describe('VM Execution', () => {
it.skip('should use VM when Freestyle API key is not available', async () => {
// Mock no Freestyle API key
vi.doMock('@/lib/env', () => ({
env: {
FREESTYLE_API_KEY: undefined,
},
}))
const req = createMockRequest('POST', {
code: 'return "vm test"',
})
const { POST } = await import('@/app/api/function/execute/route')
await POST(req)
expect(mockFreestyleExecuteScript).not.toHaveBeenCalled()
expect(mockRunInContext).toHaveBeenCalled()
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringMatching(
/\[.*\] Using VM for code execution \(no Freestyle API key available\)/
)
)
})
it('should handle VM execution errors', async () => {
// Mock no Freestyle API key so it uses VM
vi.doMock('@/lib/env', () => ({
env: {
FREESTYLE_API_KEY: undefined,
},
}))
mockRunInContext.mockRejectedValueOnce(new Error('VM execution error'))
const req = createMockRequest('POST', {
code: 'return invalidCode(',
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(500)
const data = await response.json()
expect(data.success).toBe(false)
expect(data.error).toContain('VM execution error')
})
})
describe('Custom Tools', () => {
it('should handle custom tool execution with direct parameter access', async () => {
const req = createMockRequest('POST', {
@@ -651,113 +532,3 @@ SyntaxError: Invalid or unexpected token
})
})
})
describe('Function Execute API - Template Variable Edge Cases', () => {
beforeEach(() => {
vi.resetModules()
vi.resetAllMocks()
vi.doMock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.doMock('@/lib/env', () => ({
env: {
FREESTYLE_API_KEY: 'test-freestyle-key',
},
}))
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: vi.fn().mockImplementation(() => ({
runInContext: mockRunInContext,
})),
}))
vi.doMock('freestyle-sandboxes', () => ({
FreestyleSandboxes: vi.fn().mockImplementation(() => ({
executeScript: mockFreestyleExecuteScript,
})),
}))
mockFreestyleExecuteScript.mockResolvedValue({
result: 'freestyle success',
logs: [],
})
mockRunInContext.mockResolvedValue('vm success')
mockCreateContext.mockReturnValue({})
})
it.skip('should handle nested template variables', async () => {
mockFreestyleExecuteScript.mockResolvedValueOnce({
result: 'environment-valueparam-value',
logs: [],
})
const req = createMockRequest('POST', {
code: 'return {{outer}} + <inner>',
envVars: {
outer: 'environment-value',
},
params: {
inner: 'param-value',
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.output.result).toBe('environment-valueparam-value')
})
it.skip('should prioritize environment variables over params for {{}} syntax', async () => {
mockFreestyleExecuteScript.mockResolvedValueOnce({
result: 'env-wins',
logs: [],
})
const req = createMockRequest('POST', {
code: 'return {{conflictVar}}',
envVars: {
conflictVar: 'env-wins',
},
params: {
conflictVar: 'param-loses',
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
// Environment variable should take precedence
expect(data.output.result).toBe('env-wins')
})
it.skip('should handle missing template variables gracefully', async () => {
mockFreestyleExecuteScript.mockResolvedValueOnce({
result: '',
logs: [],
})
const req = createMockRequest('POST', {
code: 'return {{nonexistent}} + <alsoMissing>',
envVars: {},
params: {},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.output.result).toBe('')
})
})

View File

@@ -367,150 +367,6 @@ export async function POST(req: NextRequest) {
const executionMethod = 'vm' // Default execution method
// // Try to use Freestyle if the API key is available
// if (env.FREESTYLE_API_KEY) {
// try {
// logger.info(`[${requestId}] Using Freestyle for code execution`)
// executionMethod = 'freestyle'
// // Extract npm packages from code if needed
// const importRegex =
// /import\s+?(?:(?:(?:[\w*\s{},]*)\s+from\s+?)|)(?:(?:"([^"]*)")|(?:'([^']*)'))[^;]*/g
// const requireRegex = /const\s+[\w\s{}]*\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
// const packages: Record<string, string> = {}
// const matches = [
// ...resolvedCode.matchAll(importRegex),
// ...resolvedCode.matchAll(requireRegex),
// ]
// // Extract package names from import statements
// for (const match of matches) {
// const packageName = match[1] || match[2]
// if (packageName && !packageName.startsWith('.') && !packageName.startsWith('/')) {
// // Extract just the package name without version or subpath
// const basePackageName = packageName.split('/')[0]
// packages[basePackageName] = 'latest' // Use latest version
// }
// }
// const freestyle = new FreestyleSandboxes({
// apiKey: env.FREESTYLE_API_KEY,
// })
// // Wrap code in export default to match Freestyle's expectations
// const wrappedCode = isCustomTool
// ? `export default async () => {
// // For custom tools, directly declare parameters as variables
// ${Object.entries(executionParams)
// .map(([key, value]) => `const ${key} = ${safeJSONStringify(value)};`)
// .join('\n ')}
// ${resolvedCode}
// }`
// : `export default async () => { ${resolvedCode} }`
// // Execute the code with Freestyle
// const res = await freestyle.executeScript(wrappedCode, {
// nodeModules: packages,
// timeout: null,
// envVars: envVars,
// })
// // Check for direct API error response
// // Type assertion since the library types don't include error response
// const response = res as { _type?: string; error?: string }
// if (response._type === 'error' && response.error) {
// logger.error(`[${requestId}] Freestyle returned error response`, {
// error: response.error,
// })
// throw response.error
// }
// // Capture stdout/stderr from Freestyle logs
// stdout =
// res.logs
// ?.map((log) => (log.type === 'error' ? 'ERROR: ' : '') + log.message)
// .join('\n') || ''
// // Check for errors reported within Freestyle logs
// const freestyleErrors = res.logs?.filter((log) => log.type === 'error') || []
// if (freestyleErrors.length > 0) {
// const errorMessage = freestyleErrors.map((log) => log.message).join('\n')
// logger.error(`[${requestId}] Freestyle execution completed with script errors`, {
// errorMessage,
// stdout,
// })
// // Create a proper Error object to be caught by the outer handler
// const scriptError = new Error(errorMessage)
// scriptError.name = 'FreestyleScriptError'
// throw scriptError
// }
// // If no errors, execution was successful
// result = res.result
// logger.info(`[${requestId}] Freestyle execution successful`, {
// result,
// stdout,
// })
// } catch (error: any) {
// // Check if the error came from our explicit throw above due to script errors
// if (error.name === 'FreestyleScriptError') {
// throw error // Re-throw to be caught by the outer handler
// }
// // Otherwise, it's likely a Freestyle API call error (network, auth, config, etc.) -> Fallback to VM
// logger.error(`[${requestId}] Freestyle API call failed, falling back to VM:`, {
// error: error.message,
// stack: error.stack,
// })
// executionMethod = 'vm_fallback'
// // Continue to VM execution
// const context = createContext({
// params: executionParams,
// environmentVariables: envVars,
// console: {
// log: (...args: any[]) => {
// const logMessage = `${args
// .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
// .join(' ')}\n`
// stdout += logMessage
// },
// error: (...args: any[]) => {
// const errorMessage = `${args
// .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
// .join(' ')}\n`
// logger.error(`[${requestId}] Code Console Error: ${errorMessage}`)
// stdout += `ERROR: ${errorMessage}`
// },
// },
// })
// const script = new Script(`
// (async () => {
// try {
// ${
// isCustomTool
// ? `// For custom tools, make parameters directly accessible
// ${Object.keys(executionParams)
// .map((key) => `const ${key} = params.${key};`)
// .join('\n ')}`
// : ''
// }
// ${resolvedCode}
// } catch (error) {
// console.error(error);
// throw error;
// }
// })()
// `)
// result = await script.runInContext(context, {
// timeout,
// displayErrors: true,
// })
// }
// } else {
logger.info(`[${requestId}] Using VM for code execution`, {
resolvedCode,
hasEnvVars: Object.keys(envVars).length > 0,

View File

@@ -3,11 +3,8 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { db } from '@/db'
export const dynamic = 'force-dynamic'
import { createErrorResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
import { apiKey as apiKeyTable } from '@/db/schema'
const logger = createLogger('TaskStatusAPI')

View File

@@ -4,9 +4,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { checkChunkAccess } from '@/app/api/knowledge/utils'
import { db } from '@/db'
import { document, embedding } from '@/db/schema'

View File

@@ -4,9 +4,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { estimateTokenCount } from '@/lib/tokenization/estimators'
import { getUserId } from '@/app/api/auth/oauth/utils'
import {

View File

@@ -4,9 +4,6 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { TAG_SLOTS } from '@/lib/constants/knowledge'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import {
checkDocumentAccess,
checkDocumentWriteAccess,

View File

@@ -21,8 +21,6 @@ import { invitation, member, organization, user, workspace, workspaceInvitation
const logger = createLogger('OrganizationInvitationsAPI')
export const dynamic = 'force-dynamic'
interface WorkspaceInvitation {
workspaceId: string
permission: 'admin' | 'write' | 'read'

View File

@@ -7,8 +7,6 @@ import { member, user, userStats } from '@/db/schema'
const logger = createLogger('OrganizationMemberAPI')
export const dynamic = 'force-dynamic'
/**
* GET /api/organizations/[id]/members/[memberId]
* Get individual organization member details

View File

@@ -13,8 +13,6 @@ import { invitation, member, organization, user, userStats } from '@/db/schema'
const logger = createLogger('OrganizationMembersAPI')
export const dynamic = 'force-dynamic'
/**
* GET /api/organizations/[id]/members
* Get organization members with optional usage data

View File

@@ -9,8 +9,6 @@ import { invitation, member, permissions, workspaceInvitation } from '@/db/schem
const logger = createLogger('OrganizationInvitationAcceptanceAPI')
export const dynamic = 'force-dynamic'
// Accept an organization invitation and any associated workspace invitations
export async function GET(req: NextRequest) {
const invitationId = req.nextUrl.searchParams.get('id')

View File

@@ -3,9 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { workflow, workflowSchedule } from '@/db/schema'

View File

@@ -18,8 +18,6 @@ import { workflow, workflowSchedule } from '@/db/schema'
const logger = createLogger('ScheduledAPI')
export const dynamic = 'force-dynamic'
const ScheduleRequestSchema = z.object({
workflowId: z.string(),
blockId: z.string().optional(),

View File

@@ -9,9 +9,6 @@ import { customTools } from '@/db/schema'
const logger = createLogger('CustomToolsAPI')
export const dynamic = 'force-dynamic'
// Define validation schema for custom tools
const CustomToolSchema = z.object({
tools: z.array(
z.object({

View File

@@ -7,8 +7,6 @@ import { isOrganizationOwnerOrAdmin } from '@/lib/permissions/utils'
const logger = createLogger('UnifiedUsageLimitsAPI')
export const dynamic = 'force-dynamic'
/**
* Unified Usage Limits Endpoint
* GET/PUT /api/usage-limits?context=user|member&userId=<id>&organizationId=<id>

View File

@@ -2,9 +2,6 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { apiKey } from '@/db/schema'

View File

@@ -9,8 +9,6 @@ import { apiKey } from '@/db/schema'
const logger = createLogger('ApiKeysAPI')
export const dynamic = 'force-dynamic'
// GET /api/users/me/api-keys - Get all API keys for the current user
export async function GET(request: NextRequest) {
try {

View File

@@ -16,7 +16,6 @@ const SettingsSchema = z.object({
autoPan: z.boolean().optional(),
consoleExpandedByDefault: z.boolean().optional(),
telemetryEnabled: z.boolean().optional(),
telemetryNotifiedUser: z.boolean().optional(),
emailPreferences: z
.object({
unsubscribeAll: z.boolean().optional(),
@@ -35,7 +34,6 @@ const defaultSettings = {
autoPan: true,
consoleExpandedByDefault: true,
telemetryEnabled: true,
telemetryNotifiedUser: false,
emailPreferences: {},
}
@@ -69,7 +67,6 @@ export async function GET() {
autoPan: userSettings.autoPan,
consoleExpandedByDefault: userSettings.consoleExpandedByDefault,
telemetryEnabled: userSettings.telemetryEnabled,
telemetryNotifiedUser: userSettings.telemetryNotifiedUser,
emailPreferences: userSettings.emailPreferences ?? {},
},
},

View File

@@ -3,9 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { member, organization, subscription } from '@/db/schema'

View File

@@ -2,9 +2,6 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { createErrorResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
import { apiKey as apiKeyTable, subscription } from '@/db/schema'

View File

@@ -1,6 +1,7 @@
import { tasks } from '@trigger.dev/sdk/v3'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkServerSideUsageLimits } from '@/lib/billing'
import { createLogger } from '@/lib/logs/console/logger'
import {
handleSlackChallenge,
@@ -245,7 +246,44 @@ export async function POST(
// Continue processing - better to risk rate limit bypass than fail webhook
}
// --- PHASE 4: Queue webhook execution via trigger.dev ---
// --- PHASE 4: Usage limit check ---
try {
const usageCheck = await checkServerSideUsageLimits(foundWorkflow.userId)
if (usageCheck.isExceeded) {
logger.warn(
`[${requestId}] User ${foundWorkflow.userId} has exceeded usage limits. Skipping webhook execution.`,
{
currentUsage: usageCheck.currentUsage,
limit: usageCheck.limit,
workflowId: foundWorkflow.id,
provider: foundWebhook.provider,
}
)
// Return 200 to prevent webhook provider retries, but indicate usage limit exceeded
if (foundWebhook.provider === 'microsoftteams') {
// Microsoft Teams requires specific response format
return NextResponse.json({
type: 'message',
text: 'Usage limit exceeded. Please upgrade your plan to continue.',
})
}
// Simple error response for other providers (return 200 to prevent retries)
return NextResponse.json({ message: 'Usage limit exceeded' }, { status: 200 })
}
logger.debug(`[${requestId}] Usage limit check passed for webhook`, {
provider: foundWebhook.provider,
currentUsage: usageCheck.currentUsage,
limit: usageCheck.limit,
})
} catch (usageError) {
logger.error(`[${requestId}] Error checking webhook usage limits:`, usageError)
// Continue processing - better to risk usage limit bypass than fail webhook
}
// --- PHASE 5: Queue webhook execution via trigger.dev ---
try {
// Queue the webhook execution task
const handle = await tasks.trigger('webhook-execution', {

View File

@@ -4,9 +4,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'

View File

@@ -12,8 +12,6 @@ import { apiKey as apiKeyTable, workflow } from '@/db/schema'
const logger = createLogger('WorkflowByIdAPI')
export const dynamic = 'force-dynamic'
const UpdateWorkflowSchema = z.object({
name: z.string().min(1, 'Name is required').optional(),
description: z.string().optional(),

View File

@@ -3,9 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
import { db } from '@/db'
@@ -13,7 +10,6 @@ import { workflow } from '@/db/schema'
const logger = createLogger('WorkflowStateAPI')
// Zod schemas for workflow state validation
const PositionSchema = z.object({
x: z.number(),
y: z.number(),

View File

@@ -3,9 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow } from '@/db/schema'

View File

@@ -365,6 +365,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
position: { x: number; y: number }
subBlocks?: Record<string, any>
data?: Record<string, any>
parentId?: string
extent?: string
}>
const edges = workflowState.edges
const warnings = conversionResult.warnings || []
@@ -395,6 +397,13 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
if (!blockConfig && (block.type === 'loop' || block.type === 'parallel')) {
// Handle loop/parallel blocks (they don't have regular block configs)
// Preserve parentId if it exists (though loop/parallel shouldn't have parents)
const containerData = block.data || {}
if (block.parentId) {
containerData.parentId = block.parentId
containerData.extent = block.extent || 'parent'
}
newWorkflowState.blocks[newId] = {
id: newId,
type: block.type,
@@ -407,7 +416,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
isWide: false,
advancedMode: false,
height: 0,
data: block.data || {},
data: containerData,
}
logger.debug(`[${requestId}] Processed loop/parallel block: ${block.id} -> ${newId}`)
} else if (blockConfig) {
@@ -440,6 +449,13 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
// Set up outputs from block configuration
const outputs = resolveOutputType(blockConfig.outputs)
// Preserve parentId if it exists in the imported block
const blockData = block.data || {}
if (block.parentId) {
blockData.parentId = block.parentId
blockData.extent = block.extent || 'parent'
}
newWorkflowState.blocks[newId] = {
id: newId,
type: block.type,
@@ -452,7 +468,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
isWide: false,
advancedMode: false,
height: 0,
data: block.data || {},
data: blockData,
}
logger.debug(`[${requestId}] Processed regular block: ${block.id} -> ${newId}`)
@@ -529,6 +545,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
}
}
// Debug: Log block parent-child relationships before generating loops
// Generate loop and parallel configurations
const loops = generateLoopBlocks(newWorkflowState.blocks)
const parallels = generateParallelBlocks(newWorkflowState.blocks)

View File

@@ -8,9 +8,6 @@ import { workflow, workflowBlocks } from '@/db/schema'
const logger = createLogger('WorkflowAPI')
export const dynamic = 'force-dynamic'
// Schema for workflow creation
const CreateWorkflowSchema = z.object({
name: z.string().min(1, 'Name is required'),
description: z.string().optional().default(''),

View File

@@ -3,9 +3,6 @@ import { and, eq, isNull } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow, workspace } from '@/db/schema'

View File

@@ -0,0 +1,210 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { simAgentClient } from '@/lib/sim-agent'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
import { resolveOutputType } from '@/blocks/utils'
import { db } from '@/db'
import { workflow } from '@/db/schema'
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
const logger = createLogger('WorkflowYamlExportAPI')
// Get API key at module level like working routes
const SIM_AGENT_API_KEY = process.env.SIM_AGENT_API_KEY
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const url = new URL(request.url)
const workflowId = url.searchParams.get('workflowId')
try {
logger.info(`[${requestId}] Exporting workflow YAML from database: ${workflowId}`)
if (!workflowId) {
return NextResponse.json({ success: false, error: 'workflowId is required' }, { status: 400 })
}
// Get the session for authentication
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Fetch the workflow from database
const workflowData = await db
.select()
.from(workflow)
.where(eq(workflow.id, workflowId))
.then((rows) => rows[0])
if (!workflowData) {
logger.warn(`[${requestId}] Workflow ${workflowId} not found`)
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
}
// Check if user has access to this workflow
let hasAccess = false
// Case 1: User owns the workflow
if (workflowData.userId === userId) {
hasAccess = true
}
// Case 2: Workflow belongs to a workspace the user has permissions for
if (!hasAccess && workflowData.workspaceId) {
const userPermission = await getUserEntityPermissions(
userId,
'workspace',
workflowData.workspaceId
)
if (userPermission !== null) {
hasAccess = true
}
}
if (!hasAccess) {
logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`)
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
// Try to load from normalized tables first
logger.debug(`[${requestId}] Attempting to load workflow ${workflowId} from normalized tables`)
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
let workflowState: any
const subBlockValues: Record<string, Record<string, any>> = {}
if (normalizedData) {
logger.debug(`[${requestId}] Found normalized data for workflow ${workflowId}:`, {
blocksCount: Object.keys(normalizedData.blocks).length,
edgesCount: normalizedData.edges.length,
})
// Use normalized table data - reconstruct complete state object
const existingState =
workflowData.state && typeof workflowData.state === 'object' ? workflowData.state : {}
workflowState = {
deploymentStatuses: {},
hasActiveWebhook: false,
...existingState,
blocks: normalizedData.blocks,
edges: normalizedData.edges,
loops: normalizedData.loops,
parallels: normalizedData.parallels,
lastSaved: Date.now(),
isDeployed: workflowData.isDeployed || false,
deployedAt: workflowData.deployedAt,
}
// Extract subblock values from the normalized blocks
Object.entries(normalizedData.blocks).forEach(([blockId, block]: [string, any]) => {
subBlockValues[blockId] = {}
if (block.subBlocks) {
Object.entries(block.subBlocks).forEach(([subBlockId, subBlock]: [string, any]) => {
if (subBlock && typeof subBlock === 'object' && 'value' in subBlock) {
subBlockValues[blockId][subBlockId] = subBlock.value
}
})
}
})
logger.info(`[${requestId}] Loaded workflow ${workflowId} from normalized tables`)
} else {
// Fallback to JSON blob
logger.info(
`[${requestId}] Using JSON blob for workflow ${workflowId} - no normalized data found`
)
if (!workflowData.state || typeof workflowData.state !== 'object') {
return NextResponse.json(
{ success: false, error: 'Workflow has no valid state data' },
{ status: 400 }
)
}
workflowState = workflowData.state as any
// Extract subblock values from JSON blob state
if (workflowState.blocks) {
Object.entries(workflowState.blocks).forEach(([blockId, block]: [string, any]) => {
subBlockValues[blockId] = {}
if (block.subBlocks) {
Object.entries(block.subBlocks).forEach(([subBlockId, subBlock]: [string, any]) => {
if (subBlock && typeof subBlock === 'object' && 'value' in subBlock) {
subBlockValues[blockId][subBlockId] = subBlock.value
}
})
}
})
}
}
// Gather block registry and utilities for sim-agent
const blocks = getAllBlocks()
const blockRegistry = blocks.reduce(
(acc, block) => {
const blockType = block.type
acc[blockType] = {
...block,
id: blockType,
subBlocks: block.subBlocks || [],
outputs: block.outputs || {},
} as any
return acc
},
{} as Record<string, BlockConfig>
)
// Call sim-agent directly
const result = await simAgentClient.makeRequest('/api/workflow/to-yaml', {
body: {
workflowState,
subBlockValues,
blockRegistry,
utilities: {
generateLoopBlocks: generateLoopBlocks.toString(),
generateParallelBlocks: generateParallelBlocks.toString(),
resolveOutputType: resolveOutputType.toString(),
},
},
apiKey: SIM_AGENT_API_KEY,
})
if (!result.success || !result.data?.yaml) {
return NextResponse.json(
{
success: false,
error: result.error || 'Failed to generate YAML',
},
{ status: result.status || 500 }
)
}
logger.info(`[${requestId}] Successfully generated YAML from database`, {
yamlLength: result.data.yaml.length,
})
return NextResponse.json({
success: true,
yaml: result.data.yaml,
})
} catch (error) {
logger.error(`[${requestId}] YAML export failed`, error)
return NextResponse.json(
{
success: false,
error: `Failed to export YAML: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
{ status: 500 }
)
}
}

View File

@@ -3,9 +3,6 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getUsersWithPermissions, hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { permissions, type permissionTypeEnum } from '@/db/schema'

View File

@@ -5,8 +5,6 @@ import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workspaceInvitation } from '@/db/schema'
export const dynamic = 'force-dynamic'
// DELETE /api/workspaces/invitations/[id] - Delete a workspace invitation
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params

View File

@@ -6,8 +6,6 @@ import { env } from '@/lib/env'
import { db } from '@/db'
import { permissions, user, workspace, workspaceInvitation } from '@/db/schema'
export const dynamic = 'force-dynamic'
// Accept an invitation via token
export async function GET(req: NextRequest) {
const token = req.nextUrl.searchParams.get('token')

View File

@@ -4,8 +4,6 @@ import { getSession } from '@/lib/auth'
import { db } from '@/db'
import { workspace, workspaceInvitation } from '@/db/schema'
export const dynamic = 'force-dynamic'
// Get invitation details by token
export async function GET(req: NextRequest) {
const token = req.nextUrl.searchParams.get('token')

View File

@@ -5,8 +5,6 @@ import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
import { db } from '@/db'
import { permissions } from '@/db/schema'
export const dynamic = 'force-dynamic'
// DELETE /api/workspaces/members/[id] - Remove a member from a workspace
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id: userId } = await params

View File

@@ -7,8 +7,6 @@ import { permissions, type permissionTypeEnum, user } from '@/db/schema'
type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
export const dynamic = 'force-dynamic'
// Add a member to a workspace
export async function POST(req: Request) {
const session = await getSession()

View File

@@ -62,9 +62,13 @@ const CreateDiffRequestSchema = z.object({
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
logger.info(`[${requestId}] ===== YAML DIFF CREATE API CALLED =====`)
try {
const body = await request.json()
logger.info(`[${requestId}] Request body received, parsing...`)
const { yamlContent, diffAnalysis, options } = CreateDiffRequestSchema.parse(body)
logger.info(`[${requestId}] Request parsed successfully`)
// Get current workflow state for comparison
// Note: This endpoint is stateless, so we need to get this from the request
@@ -151,6 +155,36 @@ export async function POST(request: NextRequest) {
// Log the full response to see if auto-layout is happening
logger.info(`[${requestId}] Full sim agent response:`, JSON.stringify(result, null, 2))
// Log detailed block information to debug parent-child relationships
if (result.success) {
const blocks = result.diff?.proposedState?.blocks || result.blocks || {}
logger.info(`[${requestId}] Sim agent blocks with parent-child info:`)
Object.entries(blocks).forEach(([blockId, block]: [string, any]) => {
if (block.data?.parentId || block.parentId) {
logger.info(`[${requestId}] Child block ${blockId} (${block.name}):`, {
type: block.type,
parentId: block.data?.parentId || block.parentId,
extent: block.data?.extent || block.extent,
hasDataField: !!block.data,
dataKeys: block.data ? Object.keys(block.data) : [],
})
}
if (block.type === 'loop' || block.type === 'parallel') {
logger.info(`[${requestId}] Container block ${blockId} (${block.name}):`, {
type: block.type,
hasData: !!block.data,
dataKeys: block.data ? Object.keys(block.data) : [],
})
}
})
// Log existing loops/parallels from sim-agent
const loops = result.diff?.proposedState?.loops || result.loops || {}
const parallels = result.diff?.proposedState?.parallels || result.parallels || {}
logger.info(`[${requestId}] Sim agent loops:`, loops)
logger.info(`[${requestId}] Sim agent parallels:`, parallels)
}
// Log diff analysis specifically
if (result.diff?.diffAnalysis) {
logger.info(`[${requestId}] Diff analysis received:`, {
@@ -164,19 +198,127 @@ export async function POST(request: NextRequest) {
logger.warn(`[${requestId}] No diff analysis in response!`)
}
// Post-process the result to ensure loops and parallels are properly generated
const finalResult = result
if (result.success && result.diff?.proposedState) {
// First, fix parent-child relationships based on edges
const blocks = result.diff.proposedState.blocks
const edges = result.diff.proposedState.edges || []
// Find all loop and parallel blocks
const containerBlocks = Object.values(blocks).filter(
(block: any) => block.type === 'loop' || block.type === 'parallel'
)
// For each container, find its children based on loop-start edges
containerBlocks.forEach((container: any) => {
// Log all edges from this container to debug
const allEdgesFromContainer = edges.filter((edge: any) => edge.source === container.id)
logger.info(
`[${requestId}] All edges from container ${container.id}:`,
allEdgesFromContainer.map((e: any) => ({
id: e.id,
sourceHandle: e.sourceHandle,
target: e.target,
}))
)
const childEdges = edges.filter(
(edge: any) => edge.source === container.id && edge.sourceHandle === 'loop-start-source'
)
childEdges.forEach((edge: any) => {
const childBlock = blocks[edge.target]
if (childBlock) {
// Ensure data field exists
if (!childBlock.data) {
childBlock.data = {}
}
// Set parentId and extent
childBlock.data.parentId = container.id
childBlock.data.extent = 'parent'
logger.info(`[${requestId}] Fixed parent-child relationship:`, {
parent: container.id,
parentName: container.name,
child: childBlock.id,
childName: childBlock.name,
})
}
})
})
// Now regenerate loops and parallels with the fixed relationships
const loops = generateLoopBlocks(result.diff.proposedState.blocks)
const parallels = generateParallelBlocks(result.diff.proposedState.blocks)
result.diff.proposedState.loops = loops
result.diff.proposedState.parallels = parallels
logger.info(`[${requestId}] Regenerated loops and parallels after fixing parent-child:`, {
loopsCount: Object.keys(loops).length,
parallelsCount: Object.keys(parallels).length,
loops: Object.keys(loops).map((id) => ({
id,
nodes: loops[id].nodes,
})),
})
}
// If the sim agent returned blocks directly (when auto-layout is applied),
// transform it to the expected diff format
if (result.success && result.blocks && !result.diff) {
logger.info(`[${requestId}] Transforming sim agent blocks response to diff format`)
// First, fix parent-child relationships based on edges
const blocks = result.blocks
const edges = result.edges || []
// Find all loop and parallel blocks
const containerBlocks = Object.values(blocks).filter(
(block: any) => block.type === 'loop' || block.type === 'parallel'
)
// For each container, find its children based on loop-start edges
containerBlocks.forEach((container: any) => {
const childEdges = edges.filter(
(edge: any) => edge.source === container.id && edge.sourceHandle === 'loop-start-source'
)
childEdges.forEach((edge: any) => {
const childBlock = blocks[edge.target]
if (childBlock) {
// Ensure data field exists
if (!childBlock.data) {
childBlock.data = {}
}
// Set parentId and extent
childBlock.data.parentId = container.id
childBlock.data.extent = 'parent'
logger.info(`[${requestId}] Fixed parent-child relationship (auto-layout):`, {
parent: container.id,
parentName: container.name,
child: childBlock.id,
childName: childBlock.name,
})
}
})
})
// Generate loops and parallels for the blocks with fixed relationships
const loops = generateLoopBlocks(result.blocks)
const parallels = generateParallelBlocks(result.blocks)
const transformedResult = {
success: result.success,
diff: {
proposedState: {
blocks: result.blocks,
edges: result.edges || [],
loops: result.loops || {},
parallels: result.parallels || {},
loops: loops,
parallels: parallels,
},
diffAnalysis: diffAnalysis,
metadata: result.metadata || {
@@ -190,7 +332,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json(transformedResult)
}
return NextResponse.json(result)
return NextResponse.json(finalResult)
} catch (error) {
logger.error(`[${requestId}] Diff creation failed:`, error)

View File

@@ -126,19 +126,146 @@ export async function POST(request: NextRequest) {
// Log the full response to see if auto-layout is happening
logger.info(`[${requestId}] Full sim agent response:`, JSON.stringify(result, null, 2))
// Log detailed block information to debug parent-child relationships
if (result.success) {
const blocks = result.diff?.proposedState?.blocks || result.blocks || {}
logger.info(`[${requestId}] Sim agent blocks with parent-child info:`)
Object.entries(blocks).forEach(([blockId, block]: [string, any]) => {
if (block.data?.parentId || block.parentId) {
logger.info(`[${requestId}] Child block ${blockId} (${block.name}):`, {
type: block.type,
parentId: block.data?.parentId || block.parentId,
extent: block.data?.extent || block.extent,
hasDataField: !!block.data,
dataKeys: block.data ? Object.keys(block.data) : [],
})
}
if (block.type === 'loop' || block.type === 'parallel') {
logger.info(`[${requestId}] Container block ${blockId} (${block.name}):`, {
type: block.type,
hasData: !!block.data,
dataKeys: block.data ? Object.keys(block.data) : [],
})
}
})
// Log existing loops/parallels from sim-agent
const loops = result.diff?.proposedState?.loops || result.loops || {}
const parallels = result.diff?.proposedState?.parallels || result.parallels || {}
logger.info(`[${requestId}] Sim agent loops:`, loops)
logger.info(`[${requestId}] Sim agent parallels:`, parallels)
}
// Post-process the result to ensure loops and parallels are properly generated
const finalResult = result
if (result.success && result.diff?.proposedState) {
// First, fix parent-child relationships based on edges
const blocks = result.diff.proposedState.blocks
const edges = result.diff.proposedState.edges || []
// Find all loop and parallel blocks
const containerBlocks = Object.values(blocks).filter(
(block: any) => block.type === 'loop' || block.type === 'parallel'
)
// For each container, find its children based on loop-start edges
containerBlocks.forEach((container: any) => {
const childEdges = edges.filter(
(edge: any) => edge.source === container.id && edge.sourceHandle === 'loop-start-source'
)
childEdges.forEach((edge: any) => {
const childBlock = blocks[edge.target]
if (childBlock) {
// Ensure data field exists
if (!childBlock.data) {
childBlock.data = {}
}
// Set parentId and extent
childBlock.data.parentId = container.id
childBlock.data.extent = 'parent'
logger.info(`[${requestId}] Fixed parent-child relationship:`, {
parent: container.id,
parentName: container.name,
child: childBlock.id,
childName: childBlock.name,
})
}
})
})
// Now regenerate loops and parallels with the fixed relationships
const loops = generateLoopBlocks(result.diff.proposedState.blocks)
const parallels = generateParallelBlocks(result.diff.proposedState.blocks)
result.diff.proposedState.loops = loops
result.diff.proposedState.parallels = parallels
logger.info(`[${requestId}] Regenerated loops and parallels after fixing parent-child:`, {
loopsCount: Object.keys(loops).length,
parallelsCount: Object.keys(parallels).length,
loops: Object.keys(loops).map((id) => ({
id,
nodes: loops[id].nodes,
})),
})
}
// If the sim agent returned blocks directly (when auto-layout is applied),
// transform it to the expected diff format
if (result.success && result.blocks && !result.diff) {
logger.info(`[${requestId}] Transforming sim agent blocks response to diff format`)
// First, fix parent-child relationships based on edges
const blocks = result.blocks
const edges = result.edges || []
// Find all loop and parallel blocks
const containerBlocks = Object.values(blocks).filter(
(block: any) => block.type === 'loop' || block.type === 'parallel'
)
// For each container, find its children based on loop-start edges
containerBlocks.forEach((container: any) => {
const childEdges = edges.filter(
(edge: any) => edge.source === container.id && edge.sourceHandle === 'loop-start-source'
)
childEdges.forEach((edge: any) => {
const childBlock = blocks[edge.target]
if (childBlock) {
// Ensure data field exists
if (!childBlock.data) {
childBlock.data = {}
}
// Set parentId and extent
childBlock.data.parentId = container.id
childBlock.data.extent = 'parent'
logger.info(`[${requestId}] Fixed parent-child relationship (auto-layout):`, {
parent: container.id,
parentName: container.name,
child: childBlock.id,
childName: childBlock.name,
})
}
})
})
// Generate loops and parallels for the blocks with fixed relationships
const loops = generateLoopBlocks(result.blocks)
const parallels = generateParallelBlocks(result.blocks)
const transformedResult = {
success: result.success,
diff: {
proposedState: {
blocks: result.blocks,
edges: result.edges || existingDiff.proposedState.edges || [],
loops: result.loops || existingDiff.proposedState.loops || {},
parallels: result.parallels || existingDiff.proposedState.parallels || {},
loops: loops,
parallels: parallels,
},
diffAnalysis: diffAnalysis,
metadata: result.metadata || {
@@ -152,7 +279,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json(transformedResult)
}
return NextResponse.json(result)
return NextResponse.json(finalResult)
} catch (error) {
logger.error(`[${requestId}] Diff merge failed:`, error)

View File

@@ -429,7 +429,8 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
if (authRequired) {
// Get title and description from the URL params or use defaults
const title = new URLSearchParams(window.location.search).get('title') || 'chat'
const primaryColor = new URLSearchParams(window.location.search).get('color') || '#802FFF'
const primaryColor =
new URLSearchParams(window.location.search).get('color') || 'var(--brand-primary-hover-hex)'
if (authRequired === 'password') {
return (

View File

@@ -18,7 +18,7 @@ export default function EmailAuth({
subdomain,
onAuthSuccess,
title = 'chat',
primaryColor = '#802FFF',
primaryColor = 'var(--brand-primary-hover-hex)',
}: EmailAuthProps) {
// Email auth state
const [email, setEmail] = useState('')
@@ -149,10 +149,10 @@ export default function EmailAuth({
xmlns='http://www.w3.org/2000/svg'
className='rounded-[6px]'
>
<rect width='50' height='50' fill='#701FFC' />
<rect width='50' height='50' fill='var(--brand-primary-hex)' />
<path
d='M34.1455 20.0728H16.0364C12.7026 20.0728 10 22.7753 10 26.1091V35.1637C10 38.4975 12.7026 41.2 16.0364 41.2H34.1455C37.4792 41.2 40.1818 38.4975 40.1818 35.1637V26.1091C40.1818 22.7753 37.4792 20.0728 34.1455 20.0728Z'
fill='#701FFC'
fill='var(--brand-primary-hex)'
stroke='white'
strokeWidth='3.5'
strokeLinecap='round'
@@ -160,7 +160,7 @@ export default function EmailAuth({
/>
<path
d='M25.0919 14.0364C26.7588 14.0364 28.1101 12.6851 28.1101 11.0182C28.1101 9.35129 26.7588 8 25.0919 8C23.425 8 22.0737 9.35129 22.0737 11.0182C22.0737 12.6851 23.425 14.0364 25.0919 14.0364Z'
fill='#701FFC'
fill='var(--brand-primary-hex)'
stroke='white'
strokeWidth='4'
strokeLinecap='round'
@@ -168,7 +168,7 @@ export default function EmailAuth({
/>
<path
d='M25.0915 14.856V19.0277V14.856ZM20.5645 32.1398V29.1216V32.1398ZM29.619 29.1216V32.1398V29.1216Z'
fill='#701FFC'
fill='var(--brand-primary-hex)'
/>
<path
d='M25.0915 14.856V19.0277M20.5645 32.1398V29.1216M29.619 29.1216V32.1398'
@@ -177,7 +177,7 @@ export default function EmailAuth({
strokeLinecap='round'
strokeLinejoin='round'
/>
<circle cx='25' cy='11' r='2' fill='#701FFC' />
<circle cx='25' cy='11' r='2' fill='var(--brand-primary-hex)' />
</svg>
</a>
</div>

View File

@@ -17,7 +17,7 @@ export default function PasswordAuth({
subdomain,
onAuthSuccess,
title = 'chat',
primaryColor = '#802FFF',
primaryColor = 'var(--brand-primary-hover-hex)',
}: PasswordAuthProps) {
// Password auth state
const [password, setPassword] = useState('')
@@ -91,10 +91,10 @@ export default function PasswordAuth({
xmlns='http://www.w3.org/2000/svg'
className='rounded-[6px]'
>
<rect width='50' height='50' fill='#701FFC' />
<rect width='50' height='50' fill='var(--brand-primary-hex)' />
<path
d='M34.1455 20.0728H16.0364C12.7026 20.0728 10 22.7753 10 26.1091V35.1637C10 38.4975 12.7026 41.2 16.0364 41.2H34.1455C37.4792 41.2 40.1818 38.4975 40.1818 35.1637V26.1091C40.1818 22.7753 37.4792 20.0728 34.1455 20.0728Z'
fill='#701FFC'
fill='var(--brand-primary-hex)'
stroke='white'
strokeWidth='3.5'
strokeLinecap='round'
@@ -102,7 +102,7 @@ export default function PasswordAuth({
/>
<path
d='M25.0919 14.0364C26.7588 14.0364 28.1101 12.6851 28.1101 11.0182C28.1101 9.35129 26.7588 8 25.0919 8C23.425 8 22.0737 9.35129 22.0737 11.0182C22.0737 12.6851 23.425 14.0364 25.0919 14.0364Z'
fill='#701FFC'
fill='var(--brand-primary-hex)'
stroke='white'
strokeWidth='4'
strokeLinecap='round'
@@ -110,7 +110,7 @@ export default function PasswordAuth({
/>
<path
d='M25.0915 14.856V19.0277V14.856ZM20.5645 32.1398V29.1216V32.1398ZM29.619 29.1216V32.1398V29.1216Z'
fill='#701FFC'
fill='var(--brand-primary-hex)'
/>
<path
d='M25.0915 14.856V19.0277M20.5645 32.1398V29.1216M29.619 29.1216V32.1398'
@@ -119,7 +119,7 @@ export default function PasswordAuth({
strokeLinecap='round'
strokeLinejoin='round'
/>
<circle cx='25' cy='11' r='2' fill='#701FFC' />
<circle cx='25' cy='11' r='2' fill='var(--brand-primary-hex)' />
</svg>
</a>
</div>

View File

@@ -21,10 +21,10 @@ export function ChatErrorState({ error, starCount }: ChatErrorStateProps) {
xmlns='http://www.w3.org/2000/svg'
className='rounded-[6px]'
>
<rect width='50' height='50' fill='#701FFC' />
<rect width='50' height='50' fill='var(--brand-primary-hex)' />
<path
d='M34.1455 20.0728H16.0364C12.7026 20.0728 10 22.7753 10 26.1091V35.1637C10 38.4975 12.7026 41.2 16.0364 41.2H34.1455C37.4792 41.2 40.1818 38.4975 40.1818 35.1637V26.1091C40.1818 22.7753 37.4792 20.0728 34.1455 20.0728Z'
fill='#701FFC'
fill='var(--brand-primary-hex)'
stroke='white'
strokeWidth='3.5'
strokeLinecap='round'
@@ -32,7 +32,7 @@ export function ChatErrorState({ error, starCount }: ChatErrorStateProps) {
/>
<path
d='M25.0919 14.0364C26.7588 14.0364 28.1101 12.6851 28.1101 11.0182C28.1101 9.35129 26.7588 8 25.0919 8C23.425 8 22.0737 9.35129 22.0737 11.0182C22.0737 12.6851 23.425 14.0364 25.0919 14.0364Z'
fill='#701FFC'
fill='var(--brand-primary-hex)'
stroke='white'
strokeWidth='4'
strokeLinecap='round'
@@ -40,7 +40,7 @@ export function ChatErrorState({ error, starCount }: ChatErrorStateProps) {
/>
<path
d='M25.0915 14.856V19.0277V14.856ZM20.5645 32.1398V29.1216V32.1398ZM29.619 29.1216V32.1398V29.1216Z'
fill='#701FFC'
fill='var(--brand-primary-hex)'
/>
<path
d='M25.0915 14.856V19.0277M20.5645 32.1398V29.1216M29.619 29.1216V32.1398'
@@ -49,7 +49,7 @@ export function ChatErrorState({ error, starCount }: ChatErrorStateProps) {
strokeLinecap='round'
strokeLinejoin='round'
/>
<circle cx='25' cy='11' r='2' fill='#701FFC' />
<circle cx='25' cy='11' r='2' fill='var(--brand-primary-hex)' />
</svg>
</a>
<ChatHeader chatConfig={null} starCount={starCount} />

View File

@@ -16,7 +16,7 @@ interface ChatHeaderProps {
}
export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
const primaryColor = chatConfig?.customizations?.primaryColor || '#701FFC'
const primaryColor = chatConfig?.customizations?.primaryColor || 'var(--brand-primary-hex)'
const customImage = chatConfig?.customizations?.imageUrl || chatConfig?.customizations?.logoUrl
return (

View File

@@ -93,6 +93,17 @@
/* Gradient Colors */
--gradient-primary: 263 85% 70%; /* More vibrant purple */
--gradient-secondary: 336 95% 65%; /* More vibrant pink */
/* Brand Colors (Default Sim Theme) */
--brand-primary-hex: #701ffc; /* Primary brand purple */
--brand-primary-hover-hex: #802fff; /* Primary brand purple hover */
--brand-secondary-hex: #6518e6; /* Secondary brand purple */
--brand-accent-hex: #9d54ff; /* Accent purple for links */
--brand-accent-hover-hex: #a66fff; /* Accent purple hover */
--brand-background-hex: #0c0c0c; /* Primary dark background */
/* UI Surface Colors */
--surface-elevated: #202020; /* Elevated surface background for dark mode */
}
/* Dark Mode Theme */
@@ -153,6 +164,17 @@
/* Gradient Colors - Adjusted for dark mode */
--gradient-primary: 263 90% 75%; /* More vibrant purple for dark mode */
--gradient-secondary: 336 100% 72%; /* More vibrant pink for dark mode */
/* Brand Colors (Same in dark mode) */
--brand-primary-hex: #701ffc; /* Primary brand purple */
--brand-primary-hover-hex: #802fff; /* Primary brand purple hover */
--brand-secondary-hex: #6518e6; /* Secondary brand purple */
--brand-accent-hex: #9d54ff; /* Accent purple for links */
--brand-accent-hover-hex: #a66fff; /* Accent purple hover */
--brand-background-hex: #0c0c0c; /* Primary dark background */
/* UI Surface Colors */
--surface-elevated: #202020; /* Elevated surface background for dark mode */
}
}
@@ -340,6 +362,60 @@ input[type="search"]::-ms-clear {
background-clip: text;
}
/* Brand Color Utilities */
.bg-brand-primary {
background-color: var(--brand-primary-hex);
}
.bg-brand-primary-hover {
background-color: var(--brand-primary-hover-hex);
}
.bg-brand-secondary {
background-color: var(--brand-secondary-hex);
}
.bg-brand-accent {
background-color: var(--brand-accent-hex);
}
.bg-brand-background {
background-color: var(--brand-background-hex);
}
.text-brand-primary {
color: var(--brand-primary-hex);
}
.text-brand-accent {
color: var(--brand-accent-hex);
}
.text-brand-accent-hover {
color: var(--brand-accent-hover-hex);
}
.border-brand-primary {
border-color: var(--brand-primary-hex);
}
.hover\:bg-brand-primary-hover:hover {
background-color: var(--brand-primary-hover-hex);
}
.hover\:bg-brand-secondary:hover {
background-color: var(--brand-secondary-hex);
}
.hover\:text-brand-accent-hover:hover {
color: var(--brand-accent-hover-hex);
}
/* Surface Utilities */
.bg-surface-elevated {
background-color: var(--surface-elevated);
}
/* Animation Classes */
.animate-pulse-ring {
animation: pulse-ring 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;

View File

@@ -8,7 +8,6 @@ import { env } from '@/lib/env'
import { isHosted } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { getAssetUrl } from '@/lib/utils'
import { TelemetryConsentDialog } from '@/app/telemetry-consent-dialog'
import '@/app/globals.css'
import { ZoomPrevention } from '@/app/zoom-prevention'
@@ -110,7 +109,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<body suppressHydrationWarning>
<BrandedLayout>
<ZoomPrevention />
<TelemetryConsentDialog />
{children}
{isHosted && (
<>

View File

@@ -11,8 +11,8 @@ export default function manifest(): MetadataRoute.Manifest {
'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',
background_color: '#701FFC', // Default Sim brand primary color
theme_color: '#701FFC', // Default Sim brand primary color
icons: [
{
src: '/favicon/android-chrome-192x192.png',

View File

@@ -1,251 +0,0 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { useGeneralStore } from '@/stores/settings/general/store'
declare global {
interface Window {
__SIM_TELEMETRY_ENABLED?: boolean
__SIM_TRACK_EVENT?: (eventName: string, properties?: Record<string, any>) => void
}
}
const logger = createLogger('TelemetryConsentDialog')
// LocalStorage key for telemetry preferences
const TELEMETRY_NOTIFIED_KEY = 'sim_telemetry_notified'
const TELEMETRY_ENABLED_KEY = 'sim_telemetry_enabled'
const trackEvent = (eventName: string, properties?: Record<string, any>) => {
if (typeof window !== 'undefined' && window.__SIM_TELEMETRY_ENABLED) {
try {
if (window.__SIM_TRACK_EVENT) {
window.__SIM_TRACK_EVENT(eventName, properties)
}
} catch (error) {
logger.error(`Failed to track event ${eventName}:`, error)
}
}
}
export function TelemetryConsentDialog() {
const [open, setOpen] = useState(false)
const [settingsLoaded, setSettingsLoaded] = useState(false)
const telemetryEnabled = useGeneralStore((state) => state.telemetryEnabled)
const telemetryNotifiedUser = useGeneralStore((state) => state.telemetryNotifiedUser)
const setTelemetryEnabled = useGeneralStore((state) => state.setTelemetryEnabled)
const setTelemetryNotifiedUser = useGeneralStore((state) => state.setTelemetryNotifiedUser)
const loadSettings = useGeneralStore((state) => state.loadSettings)
const hasShownDialogThisSession = useRef(false)
const isChatSubdomainOrPath =
typeof window !== 'undefined' &&
(window.location.pathname.startsWith('/chat/') ||
(window.location.hostname !== 'sim.ai' &&
window.location.hostname !== 'localhost' &&
window.location.hostname !== '127.0.0.1' &&
!window.location.hostname.startsWith('www.')))
// Check localStorage for saved preferences
useEffect(() => {
if (typeof window === 'undefined' || isChatSubdomainOrPath) return
try {
const notified = localStorage.getItem(TELEMETRY_NOTIFIED_KEY) === 'true'
const enabled = localStorage.getItem(TELEMETRY_ENABLED_KEY)
if (notified) {
setTelemetryNotifiedUser(true)
}
if (enabled !== null) {
setTelemetryEnabled(enabled === 'true')
}
} catch (error) {
logger.error('Error reading telemetry preferences from localStorage:', error)
}
}, [setTelemetryNotifiedUser, setTelemetryEnabled, isChatSubdomainOrPath])
useEffect(() => {
// Skip settings loading on chat subdomain pages
if (isChatSubdomainOrPath) {
setSettingsLoaded(true)
return
}
let isMounted = true
const fetchSettings = async () => {
try {
await loadSettings(true)
if (isMounted) {
setSettingsLoaded(true)
}
} catch (error) {
logger.error('Failed to load settings:', error)
if (isMounted) {
setSettingsLoaded(true)
}
}
}
fetchSettings()
return () => {
isMounted = false
}
}, [loadSettings, isChatSubdomainOrPath])
useEffect(() => {
if (!settingsLoaded || isChatSubdomainOrPath) return
logger.debug('Settings loaded state:', {
telemetryNotifiedUser,
telemetryEnabled,
hasShownInSession: hasShownDialogThisSession.current,
environment: isDev,
})
const localStorageNotified =
typeof window !== 'undefined' && localStorage.getItem(TELEMETRY_NOTIFIED_KEY) === 'true'
// Only show dialog if:
// 1. Settings are fully loaded from the database
// 2. User has not been notified yet (according to database AND localStorage)
// 3. Telemetry is currently enabled (default)
// 4. Dialog hasn't been shown in this session already (extra protection)
// 5. We're in development environment
if (
settingsLoaded &&
!telemetryNotifiedUser &&
!localStorageNotified &&
telemetryEnabled &&
!hasShownDialogThisSession.current &&
isDev
) {
setOpen(true)
hasShownDialogThisSession.current = true
} else if (settingsLoaded && !telemetryNotifiedUser && !isDev) {
// Auto-notify in non-development environments
setTelemetryNotifiedUser(true)
if (typeof window !== 'undefined') {
try {
localStorage.setItem(TELEMETRY_NOTIFIED_KEY, 'true')
} catch (error) {
logger.error('Error saving telemetry notification to localStorage:', error)
}
}
}
}, [
settingsLoaded,
telemetryNotifiedUser,
telemetryEnabled,
setTelemetryNotifiedUser,
isChatSubdomainOrPath,
])
const handleAccept = () => {
trackEvent('telemetry_consent_accepted', {
source: 'consent_dialog',
defaultEnabled: true,
})
setTelemetryNotifiedUser(true)
setOpen(false)
// Save preference to localStorage
if (typeof window !== 'undefined') {
try {
localStorage.setItem(TELEMETRY_NOTIFIED_KEY, 'true')
localStorage.setItem(TELEMETRY_ENABLED_KEY, 'true')
} catch (error) {
logger.error('Error saving telemetry preferences to localStorage:', error)
}
}
}
const handleDecline = () => {
trackEvent('telemetry_consent_declined', {
source: 'consent_dialog',
defaultEnabled: false,
})
setTelemetryEnabled(false)
setTelemetryNotifiedUser(true)
setOpen(false)
// Save preference to localStorage
if (typeof window !== 'undefined') {
try {
localStorage.setItem(TELEMETRY_NOTIFIED_KEY, 'true')
localStorage.setItem(TELEMETRY_ENABLED_KEY, 'false')
} catch (error) {
logger.error('Error saving telemetry preferences to localStorage:', error)
}
}
}
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent className='max-w-md'>
<AlertDialogHeader>
<AlertDialogTitle className='mb-2 font-bold text-2xl'>Telemetry</AlertDialogTitle>
</AlertDialogHeader>
<div className='space-y-4 text-base text-muted-foreground'>
<div>
To help us improve Sim, we collect anonymous usage data by default. This helps us
understand which features are most useful and identify areas for improvement.
</div>
<div className='py-2'>
<div className='mb-2 font-semibold text-foreground'>We only collect:</div>
<ul className='list-disc space-y-1 pl-6'>
<li>Feature usage statistics</li>
<li>Error reports (without personal info)</li>
<li>Performance metrics</li>
</ul>
</div>
<div className='py-2'>
<div className='mb-2 font-semibold text-foreground'>We never collect:</div>
<ul className='list-disc space-y-1 pl-6'>
<li>Personal information</li>
<li>Workflow content or outputs</li>
<li>API keys or tokens</li>
<li>IP addresses or location data</li>
</ul>
</div>
<div className='pt-2 text-muted-foreground text-sm'>
You can change this setting anytime in{' '}
<span className='font-medium'>Settings Privacy</span>.
</div>
</div>
<AlertDialogFooter className='mt-4 flex flex-col gap-3 sm:flex-row'>
<AlertDialogCancel asChild onClick={handleDecline}>
<Button variant='outline' className='flex-1'>
Disable telemetry
</Button>
</AlertDialogCancel>
<AlertDialogAction asChild onClick={handleAccept}>
<Button className='flex-1'>Continue with telemetry</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -166,7 +166,7 @@ function UnsubscribeContent() {
'_blank'
)
}
className='w-full bg-[#701ffc] font-medium text-white shadow-sm transition-colors duration-200 hover:bg-[#802FFF]'
className='w-full bg-[var(--brand-primary-hex)] font-medium text-white shadow-sm transition-colors duration-200 hover:bg-[var(--brand-primary-hover-hex)]'
>
Contact Support
</Button>

View File

@@ -192,7 +192,7 @@ export function CreateChunkModal({
<Button
onClick={handleCreateChunk}
disabled={!isFormValid || isCreating}
className='bg-[#701FFC] font-[480] text-primary-foreground shadow-[0_0_0_0_#701FFC] transition-all duration-200 hover:bg-[#6518E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
className='bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-secondary-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
>
{isCreating ? (
<>

View File

@@ -66,7 +66,7 @@ export function DocumentLoading({
<Button
disabled
size='sm'
className='flex items-center gap-1 bg-[#701FFC] font-[480] text-primary-foreground shadow-[0_0_0_0_#701FFC] transition-all duration-200 hover:bg-[#6518E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50'
className='flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50'
>
<Plus className='h-3.5 w-3.5' />
<span>Create Chunk</span>

View File

@@ -312,7 +312,7 @@ export function EditChunkModal({
<Button
onClick={handleSaveContent}
disabled={!isFormValid || isSaving || !hasUnsavedChanges || isNavigating}
className='bg-[#701FFC] font-[480] text-primary-foreground shadow-[0_0_0_0_#701FFC] transition-all duration-200 hover:bg-[#6518E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
className='bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
>
{isSaving ? (
<>

View File

@@ -314,7 +314,7 @@ export function Document({
onCheckedChange={(checked) => handleSelectChunk(chunk.id, checked as boolean)}
disabled={!userPermissions.canEdit}
aria-label={`Select chunk ${chunk.chunkIndex}`}
className='h-3.5 w-3.5 border-gray-300 focus-visible:ring-[#701FFC]/20 data-[state=checked]:border-[#701FFC] data-[state=checked]:bg-[#701FFC] [&>*]:h-3 [&>*]:w-3'
className='h-3.5 w-3.5 border-gray-300 focus-visible:ring-[var(--brand-primary-hex)]/20 data-[state=checked]:border-[var(--brand-primary-hex)] data-[state=checked]:bg-[var(--brand-primary-hex)] [&>*]:h-3 [&>*]:w-3'
onClick={(e) => e.stopPropagation()}
/>
</td>
@@ -685,7 +685,7 @@ export function Document({
onClick={() => setIsCreateChunkModalOpen(true)}
disabled={documentData?.processingStatus === 'failed' || !userPermissions.canEdit}
size='sm'
className='flex items-center gap-1 bg-[#701FFC] font-[480] text-white shadow-[0_0_0_0_#701FFC] transition-all duration-200 hover:bg-[#6518E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:cursor-not-allowed disabled:opacity-50'
className='flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-white shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-secondary-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:cursor-not-allowed disabled:opacity-50'
>
<Plus className='h-3.5 w-3.5' />
<span>Create Chunk</span>
@@ -726,7 +726,7 @@ export function Document({
!userPermissions.canEdit
}
aria-label='Select all chunks'
className='h-3.5 w-3.5 border-gray-300 focus-visible:ring-[#701FFC]/20 data-[state=checked]:border-[#701FFC] data-[state=checked]:bg-[#701FFC] [&>*]:h-3 [&>*]:w-3'
className='h-3.5 w-3.5 border-gray-300 focus-visible:ring-[var(--brand-primary-hex)]/20 data-[state=checked]:border-[var(--brand-primary-hex)] data-[state=checked]:bg-[var(--brand-primary-hex)] [&>*]:h-3 [&>*]:w-3'
/>
</th>
<th className='px-4 pt-2 pb-3 text-left font-medium'>

View File

@@ -86,7 +86,7 @@ const getStatusDisplay = (doc: DocumentData) => {
</>
),
className:
'inline-flex items-center rounded-md bg-[#701FFC]/10 px-2 py-1 text-xs font-medium text-[#701FFC] dark:bg-[#701FFC]/20 dark:text-[#8B5FFF]',
'inline-flex items-center rounded-md bg-[var(--brand-primary-hex)]/10 px-2 py-1 text-xs font-medium text-[var(--brand-primary-hex)] dark:bg-[var(--brand-primary-hex)]/20 dark:text-[var(--brand-primary-hex)]',
}
case 'failed':
return {
@@ -729,7 +729,7 @@ export function KnowledgeBase({
onCheckedChange={handleSelectAll}
disabled={!userPermissions.canEdit}
aria-label='Select all documents'
className='h-3.5 w-3.5 border-gray-300 focus-visible:ring-[#701FFC]/20 data-[state=checked]:border-[#701FFC] data-[state=checked]:bg-[#701FFC] [&>*]:h-3 [&>*]:w-3'
className='h-3.5 w-3.5 border-gray-300 focus-visible:ring-[var(--brand-primary-hex)]/20 data-[state=checked]:border-[var(--brand-primary-hex)] data-[state=checked]:bg-[var(--brand-primary-hex)] [&>*]:h-3 [&>*]:w-3'
/>
</th>
<th className='px-4 pt-2 pb-3 text-left font-medium'>
@@ -886,7 +886,7 @@ export function KnowledgeBase({
disabled={!userPermissions.canEdit}
onClick={(e) => e.stopPropagation()}
aria-label={`Select ${doc.filename}`}
className='h-3.5 w-3.5 border-gray-300 focus-visible:ring-[#701FFC]/20 data-[state=checked]:border-[#701FFC] data-[state=checked]:bg-[#701FFC] [&>*]:h-3 [&>*]:w-3'
className='h-3.5 w-3.5 border-gray-300 focus-visible:ring-[var(--brand-primary-hex)]/20 data-[state=checked]:border-[var(--brand-primary-hex)] data-[state=checked]:bg-[var(--brand-primary-hex)] [&>*]:h-3 [&>*]:w-3'
/>
</td>

View File

@@ -57,7 +57,7 @@ export function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoading
<Button
disabled
size='sm'
className='flex items-center gap-1 bg-[#701FFC] font-[480] text-primary-foreground shadow-[0_0_0_0_#701FFC] transition-all duration-200 hover:bg-[#6518E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50'
className='flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50'
>
<div className='h-3.5 w-3.5 animate-pulse rounded bg-primary-foreground/30' />
<span>Add Documents</span>

View File

@@ -612,7 +612,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
<Button
type='submit'
disabled={isSubmitting}
className='bg-[#701FFC] font-[480] text-primary-foreground shadow-[0_0_0_0_#701FFC] transition-all duration-200 hover:bg-[#6518E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
className='bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
>
{isSubmitting ? 'Creating...' : 'Create Knowledge Base'}
</Button>

View File

@@ -27,7 +27,7 @@ export function PrimaryButton({
disabled={disabled}
size={size}
className={cn(
'flex items-center gap-1 bg-[#701FFC] font-[480] text-white shadow-[0_0_0_0_#701FFC] transition-all duration-200 hover:bg-[#6518E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-white shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
disabled && 'disabled:cursor-not-allowed disabled:opacity-50',
className
)}

View File

@@ -33,7 +33,7 @@ export function SearchInput({
/>
{isLoading ? (
<div className='-translate-y-1/2 absolute top-1/2 right-3'>
<div className='h-[18px] w-[18px] animate-spin rounded-full border-2 border-gray-300 border-t-[#701FFC]' />
<div className='h-[18px] w-[18px] animate-spin rounded-full border-2 border-gray-300 border-t-[var(--brand-primary-hex)]' />
</div>
) : (
value &&

View File

@@ -37,7 +37,7 @@ export default function KnowledgeLoading() {
<Button
disabled
size='sm'
className='flex items-center gap-1 bg-[#701FFC] font-[480] text-primary-foreground shadow-[0_0_0_0_#701FFC] transition-all duration-200 hover:bg-[#6518E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50'
className='flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50'
>
<Plus className='h-3.5 w-3.5' />
<span>Create</span>

View File

@@ -97,7 +97,7 @@ export default function FolderFilter() {
<Button
variant='outline'
size='sm'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[#202020]'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
{loading ? 'Loading folders...' : getSelectedFoldersText()}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
@@ -105,7 +105,7 @@ export default function FolderFilter() {
</DropdownMenuTrigger>
<DropdownMenuContent
align='start'
className='max-h-[300px] w-[200px] overflow-y-auto rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[#202020]'
className='max-h-[300px] w-[200px] overflow-y-auto rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<DropdownMenuItem
key='all'

View File

@@ -29,7 +29,7 @@ export default function Level() {
<Button
variant='outline'
size='sm'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[#202020]'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
{getDisplayLabel()}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
@@ -37,7 +37,7 @@ export default function Level() {
</DropdownMenuTrigger>
<DropdownMenuContent
align='start'
className='w-[180px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[#202020]'
className='w-[180px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<DropdownMenuItem
key='all'

View File

@@ -20,7 +20,7 @@ export default function Timeline() {
<Button
variant='outline'
size='sm'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[#202020]'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
{timeRange}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
@@ -28,7 +28,7 @@ export default function Timeline() {
</DropdownMenuTrigger>
<DropdownMenuContent
align='start'
className='w-[180px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[#202020]'
className='w-[180px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<DropdownMenuItem
key='all'

View File

@@ -46,7 +46,7 @@ export default function Trigger() {
<Button
variant='outline'
size='sm'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[#202020]'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
{getSelectedTriggersText()}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
@@ -54,7 +54,7 @@ export default function Trigger() {
</DropdownMenuTrigger>
<DropdownMenuContent
align='start'
className='w-[180px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[#202020]'
className='w-[180px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<DropdownMenuItem
key='all'

View File

@@ -72,7 +72,7 @@ export default function Workflow() {
<Button
variant='outline'
size='sm'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[#202020]'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
{loading ? 'Loading workflows...' : getSelectedWorkflowsText()}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
@@ -80,7 +80,7 @@ export default function Workflow() {
</DropdownMenuTrigger>
<DropdownMenuContent
align='start'
className='max-h-[300px] w-[180px] overflow-y-auto rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[#202020]'
className='max-h-[300px] w-[180px] overflow-y-auto rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
style={{
scrollbarWidth: 'none',
msOverflowStyle: 'none',

View File

@@ -393,7 +393,7 @@ function TraceSpanItem({
// Block type specific icons
if (type === 'agent') {
return <AgentIcon className='h-3 w-3 text-[#802FFF]' />
return <AgentIcon className='h-3 w-3 text-[var(--brand-primary-hover-hex)]' />
}
if (type === 'evaluator') {
@@ -437,7 +437,7 @@ function TraceSpanItem({
const getSpanColor = (type: string) => {
switch (type.toLowerCase()) {
case 'agent':
return '#802FFF' // Purple from AgentBlock
return 'var(--brand-primary-hover-hex)' // Purple from AgentBlock
case 'provider':
return '#818cf8' // Indigo for provider
case 'model':

View File

@@ -458,8 +458,10 @@ export default function Logs() {
</Tooltip>
<Button
className={`group h-9 gap-2 rounded-[11px] border bg-card text-card-foreground shadow-xs transition-all duration-200 hover:border-[#701FFC] hover:bg-[#701FFC] hover:text-white ${
isLive ? 'border-[#701FFC] bg-[#701FFC] text-white' : 'border-border'
className={`group h-9 gap-2 rounded-[11px] border bg-card text-card-foreground shadow-xs transition-all duration-200 hover:border-[var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hex)] hover:text-white ${
isLive
? 'border-[var(--brand-primary-hex)] bg-[var(--brand-primary-hex)] text-white'
: 'border-border'
}`}
onClick={toggleLive}
>

View File

@@ -412,8 +412,8 @@ export function TemplateCard({
onClick={handleUseClick}
className={cn(
'rounded-[8px] px-3 py-1 font-medium font-sans text-white text-xs transition-[background-color,box-shadow] duration-200',
'bg-[#701FFC] hover:bg-[#6518E6]',
'shadow-[0_0_0_0_#701FFC] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
'bg-[var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hover-hex)]',
'shadow-[0_0_0_0_var(--brand-primary-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
)}
>
Use

View File

@@ -234,7 +234,7 @@ export default function Templates({ initialTemplates, currentUserId }: Templates
</div>
{/* <Button
onClick={handleCreateNew}
className='flex h-9 items-center gap-2 rounded-lg bg-[#701FFC] px-4 py-2 font-normal font-sans text-sm text-white hover:bg-[#601EE0]'
className='flex h-9 items-center gap-2 rounded-lg bg-[var(--brand-primary-hex)] px-4 py-2 font-normal font-sans text-sm text-white hover:bg-[#601EE0]'
>
<Plus className='h-4 w-4' />
Create New

View File

@@ -79,7 +79,7 @@ export function useChatDeployment() {
title: formData.title.trim(),
description: formData.description.trim(),
customizations: {
primaryColor: '#802FFF',
primaryColor: 'var(--brand-primary-hover-hex)',
welcomeMessage: formData.welcomeMessage.trim(),
...(imageUrl && { imageUrl }),
},

View File

@@ -512,10 +512,10 @@ export function DeployModal({
disabled={isSubmitting || (!keysLoaded && !apiKeys.length)}
className={cn(
'gap-2 font-medium',
'bg-[#802FFF] hover:bg-[#7028E6]',
'shadow-[0_0_0_0_#802FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'bg-[var(--brand-primary-hover-hex)] hover:bg-[var(--brand-primary-hover-hex)]',
'shadow-[0_0_0_0_var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'text-white transition-all duration-200',
'disabled:opacity-50 disabled:hover:bg-[#802FFF] disabled:hover:shadow-none'
'disabled:opacity-50 disabled:hover:bg-[var(--brand-primary-hover-hex)] disabled:hover:shadow-none'
)}
>
{isSubmitting ? (
@@ -569,10 +569,10 @@ export function DeployModal({
disabled={chatSubmitting || !isChatFormValid}
className={cn(
'gap-2 font-medium',
'bg-[#802FFF] hover:bg-[#7028E6]',
'shadow-[0_0_0_0_#802FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'bg-[var(--brand-primary-hover-hex)] hover:bg-[var(--brand-primary-hover-hex)]',
'shadow-[0_0_0_0_var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'text-white transition-all duration-200',
'disabled:opacity-50 disabled:hover:bg-[#802FFF] disabled:hover:shadow-none'
'disabled:opacity-50 disabled:hover:bg-[var(--brand-primary-hover-hex)] disabled:hover:shadow-none'
)}
>
{chatSubmitting ? (

View File

@@ -91,9 +91,9 @@ export function DeploymentControls({
disabled={isDisabled}
className={cn(
'h-12 w-12 rounded-[11px] border bg-card text-card-foreground shadow-xs',
'hover:border-[#701FFC] hover:bg-[#701FFC] hover:text-white',
'hover:border-[var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hex)] hover:text-white',
'transition-all duration-200',
isDeployed && 'text-[#802FFF]',
isDeployed && 'text-[var(--brand-primary-hover-hex)]',
isDisabled &&
'cursor-not-allowed opacity-50 hover:border hover:bg-card hover:text-card-foreground hover:shadow-xs'
)}

View File

@@ -6,7 +6,6 @@ import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console/logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store'
const logger = createLogger('ExportControls')
@@ -17,7 +16,6 @@ interface ExportControlsProps {
export function ExportControls({ disabled = false }: ExportControlsProps) {
const [isExporting, setIsExporting] = useState(false)
const { workflows, activeWorkflowId } = useWorkflowRegistry()
const getYaml = useWorkflowYamlStore((state) => state.getYaml)
const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null
@@ -45,11 +43,23 @@ export function ExportControls({ disabled = false }: ExportControlsProps) {
setIsExporting(true)
try {
const yamlContent = await getYaml()
const filename = `${currentWorkflow.name.replace(/[^a-z0-9]/gi, '-')}.yaml`
// Use the new database-based export endpoint
const response = await fetch(`/api/workflows/yaml/export?workflowId=${activeWorkflowId}`)
downloadFile(yamlContent, filename, 'text/yaml')
logger.info('Workflow exported as YAML')
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.error || `Failed to export YAML: ${response.statusText}`)
}
const result = await response.json()
if (!result.success || !result.yaml) {
throw new Error(result.error || 'Failed to export YAML')
}
const filename = `${currentWorkflow.name.replace(/[^a-z0-9]/gi, '-')}.yaml`
downloadFile(result.yaml, filename, 'text/yaml')
logger.info('Workflow exported as YAML from database')
} catch (error) {
logger.error('Failed to export workflow as YAML:', error)
} finally {

View File

@@ -410,10 +410,10 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
disabled={isSubmitting}
className={cn(
'font-medium',
'bg-[#701FFC] hover:bg-[#6518E6]',
'shadow-[0_0_0_0_#701FFC] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'bg-[var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hover-hex)]',
'shadow-[0_0_0_0_var(--brand-primary-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'text-white transition-all duration-200',
'disabled:opacity-50 disabled:hover:bg-[#701FFC] disabled:hover:shadow-none',
'disabled:opacity-50 disabled:hover:bg-[var(--brand-primary-hex)] disabled:hover:shadow-none',
'h-10 rounded-md px-4 py-2'
)}
>

View File

@@ -642,10 +642,10 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
const debugButtonClass = cn(
'h-12 w-12 rounded-[11px] font-medium',
'bg-[#701FFC] hover:bg-[#6518E6]',
'shadow-[0_0_0_0_#701FFC] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'bg-[var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hover-hex)]',
'shadow-[0_0_0_0_var(--brand-primary-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]]',
'text-white transition-all duration-200',
'disabled:opacity-50 disabled:hover:bg-[#701FFC] disabled:hover:shadow-none'
'disabled:opacity-50 disabled:hover:bg-[var(--brand-primary-hex)] disabled:hover:shadow-none'
)
return (
@@ -869,10 +869,10 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
<Button
className={cn(
'gap-2 font-medium',
'bg-[#701FFC] hover:bg-[#6518E6]',
'shadow-[0_0_0_0_#701FFC] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'bg-[var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hover-hex)]',
'shadow-[0_0_0_0_var(--brand-primary-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'text-white transition-all duration-200',
'disabled:opacity-50 disabled:hover:bg-[#701FFC] disabled:hover:shadow-none',
'disabled:opacity-50 disabled:hover:bg-[var(--brand-primary-hex)] disabled:hover:shadow-none',
'h-12 rounded-[11px] px-4 py-2'
)}
onClick={handleRunClick}

View File

@@ -187,35 +187,33 @@ export function DiffControls() {
}
}
const handleAccept = () => {
logger.info('Accepting proposed changes (optimistic)')
const handleAccept = async () => {
logger.info('Accepting proposed changes with backup protection')
// Create checkpoint in the background (don't await to avoid blocking)
createCheckpoint()
.then((checkpointCreated) => {
if (!checkpointCreated) {
logger.warn('Checkpoint creation failed, but proceeding with accept')
} else {
logger.info('Checkpoint created successfully before accept')
}
})
.catch((error) => {
logger.error('Checkpoint creation failed:', error)
try {
// Clear preview YAML immediately
await clearPreviewYaml().catch((error) => {
logger.warn('Failed to clear preview YAML:', error)
})
// Clear preview YAML immediately
clearPreviewYaml().catch((error) => {
logger.warn('Failed to clear preview YAML:', error)
})
// Accept changes with automatic backup and rollback on failure
await acceptChanges()
// Start background save without awaiting
acceptChanges().catch((error) => {
logger.error('Failed to accept changes in background:', error)
// TODO: Consider showing a toast notification for save failures
// For now, the optimistic update stands since the UI state is already correct
})
logger.info('Successfully accepted and saved workflow changes')
// Show success feedback if needed
} catch (error) {
logger.error('Failed to accept changes:', error)
logger.info('Optimistically applied changes, saving in background')
// Show error notification to user
// Note: The acceptChanges function has already rolled back the state
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
// You could add toast notification here
console.error('Workflow update failed:', errorMessage)
// Optionally show user-facing error dialog
alert(`Failed to save workflow changes: ${errorMessage}`)
}
}
const handleReject = () => {

View File

@@ -648,9 +648,9 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
}}
onKeyDown={handleKeyPress}
placeholder={isDragOver ? 'Drop files here...' : 'Type a message...'}
className={`h-9 flex-1 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] text-muted-foreground shadow-xs focus-visible:ring-0 focus-visible:ring-offset-0 dark:border-[#414141] dark:bg-[#202020] ${
className={`h-9 flex-1 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] text-muted-foreground shadow-xs focus-visible:ring-0 focus-visible:ring-offset-0 dark:border-[#414141] dark:bg-[var(--surface-elevated)] ${
isDragOver
? 'border-[#802FFF] bg-purple-50/50 dark:border-[#802FFF] dark:bg-purple-950/20'
? 'border-[var(--brand-primary-hover-hex)] bg-purple-50/50 dark:border-[var(--brand-primary-hover-hex)] dark:bg-purple-950/20'
: ''
}`}
disabled={!activeWorkflowId || isExecuting || isUploadingFiles}
@@ -664,7 +664,7 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
isExecuting ||
isUploadingFiles
}
className='h-9 w-9 rounded-lg bg-[#802FFF] text-white shadow-[0_0_0_0_#802FFF] transition-all duration-200 hover:bg-[#7028E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
className='h-9 w-9 rounded-lg bg-[var(--brand-primary-hover-hex)] text-white shadow-[0_0_0_0_var(--brand-primary-hover-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
>
<ArrowUp className='h-4 w-4' />
</Button>

View File

@@ -327,8 +327,8 @@ export function OutputSelect({
onClick={() => setIsOutputDropdownOpen(!isOutputDropdownOpen)}
className={`flex h-9 w-full items-center justify-between rounded-[8px] border px-3 py-1.5 font-normal text-sm shadow-xs transition-colors ${
isOutputDropdownOpen
? 'border-[#E5E5E5] bg-[#FFFFFF] text-muted-foreground dark:border-[#414141] dark:bg-[#202020]'
: 'border-[#E5E5E5] bg-[#FFFFFF] text-muted-foreground hover:text-muted-foreground dark:border-[#414141] dark:bg-[#202020]'
? 'border-[#E5E5E5] bg-[#FFFFFF] text-muted-foreground dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
: 'border-[#E5E5E5] bg-[#FFFFFF] text-muted-foreground hover:text-muted-foreground dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
}`}
disabled={workflowOutputs.length === 0 || disabled}
>
@@ -362,7 +362,7 @@ export function OutputSelect({
</button>
{isOutputDropdownOpen && workflowOutputs.length > 0 && (
<div className='absolute left-0 z-50 mt-1 w-full overflow-hidden rounded-[8px] border border-[#E5E5E5] bg-[#FFFFFF] pt-1 shadow-xs dark:border-[#414141] dark:bg-[#202020]'>
<div className='absolute left-0 z-50 mt-1 w-full overflow-hidden rounded-[8px] border border-[#E5E5E5] bg-[#FFFFFF] pt-1 shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'>
<div className='max-h-[230px] overflow-y-auto'>
{Object.entries(groupedOutputs).map(([blockName, outputs]) => (
<div key={blockName}>

View File

@@ -39,6 +39,8 @@ if (typeof document !== 'undefined') {
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
text-rendering: optimizeLegibility !important;
max-width: 100% !important;
overflow: auto !important;
}
.dark .copilot-markdown-wrapper pre {
@@ -58,6 +60,24 @@ if (typeof document !== 'undefined') {
-moz-osx-font-smoothing: grayscale !important;
text-rendering: optimizeLegibility !important;
}
/* Prevent any markdown content from expanding beyond the panel */
.copilot-markdown-wrapper, .copilot-markdown-wrapper * {
max-width: 100% !important;
}
.copilot-markdown-wrapper p, .copilot-markdown-wrapper li {
overflow-wrap: anywhere !important;
word-break: break-word !important;
}
.copilot-markdown-wrapper a {
overflow-wrap: anywhere !important;
word-break: break-all !important;
}
.copilot-markdown-wrapper code:not(pre code) {
white-space: normal !important;
overflow-wrap: anywhere !important;
word-break: break-word !important;
}
`
document.head.appendChild(style)
}
@@ -70,7 +90,7 @@ function LinkWithPreview({ href, children }: { href: string; children: React.Rea
<TooltipTrigger asChild>
<a
href={href}
className='text-blue-600 hover:underline dark:text-blue-400'
className='inline break-all text-blue-600 hover:underline dark:text-blue-400'
target='_blank'
rel='noopener noreferrer'
>
@@ -257,7 +277,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
if (inline) {
return (
<code
className='rounded bg-gray-200 px-1 py-0.5 font-mono text-[0.9em] text-gray-800 dark:bg-gray-700 dark:text-gray-200'
className='whitespace-normal break-all rounded bg-gray-200 px-1 py-0.5 font-mono text-[0.9em] text-gray-800 dark:bg-gray-700 dark:text-gray-200'
{...props}
>
{children}

View File

@@ -0,0 +1,95 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { Brain } from 'lucide-react'
import { cn } from '@/lib/utils'
interface ThinkingBlockProps {
content: string
isStreaming?: boolean
duration?: number // Persisted duration from content block
startTime?: number // Persisted start time from content block
}
export function ThinkingBlock({
content,
isStreaming = false,
duration: persistedDuration,
startTime: persistedStartTime,
}: ThinkingBlockProps) {
const [isExpanded, setIsExpanded] = useState(false)
const [duration, setDuration] = useState(persistedDuration ?? 0)
// Keep a stable reference to start time that updates when prop changes
const startTimeRef = useRef<number>(persistedStartTime ?? Date.now())
useEffect(() => {
if (typeof persistedStartTime === 'number') {
startTimeRef.current = persistedStartTime
}
}, [persistedStartTime])
useEffect(() => {
// If we already have a persisted duration, just use it
if (typeof persistedDuration === 'number') {
setDuration(persistedDuration)
return
}
if (isStreaming) {
const interval = setInterval(() => {
setDuration(Date.now() - startTimeRef.current)
}, 100)
return () => clearInterval(interval)
}
// Not streaming and no persisted duration: compute final duration once
setDuration(Date.now() - startTimeRef.current)
}, [isStreaming, persistedDuration])
// Format duration
const formatDuration = (ms: number) => {
if (ms < 1000) return `${ms}ms`
const seconds = (ms / 1000).toFixed(1)
return `${seconds}s`
}
if (!isExpanded) {
return (
<button
onClick={() => setIsExpanded(true)}
className={cn(
'inline-flex items-center gap-1 text-gray-400 text-xs transition-colors hover:text-gray-500',
'font-normal italic'
)}
type='button'
>
<Brain className='h-3 w-3' />
<span>Thought for {formatDuration(duration)}</span>
{isStreaming && (
<span className='inline-flex h-1 w-1 animate-pulse rounded-full bg-gray-400' />
)}
</button>
)
}
return (
<div className='my-1'>
<button
onClick={() => setIsExpanded(false)}
className={cn(
'mb-1 inline-flex items-center gap-1 text-gray-400 text-xs transition-colors hover:text-gray-500',
'font-normal italic'
)}
type='button'
>
<Brain className='h-3 w-3' />
<span>Thought for {formatDuration(duration)} (click to collapse)</span>
</button>
<div className='ml-1 border-gray-200 border-l-2 pl-2 dark:border-gray-700'>
<pre className='whitespace-pre-wrap font-mono text-gray-400 text-xs dark:text-gray-500'>
{content}
{isStreaming && <span className='ml-1 inline-block h-2 w-1 animate-pulse bg-gray-400' />}
</pre>
</div>
</div>
)
}

View File

@@ -18,6 +18,7 @@ import { usePreviewStore } from '@/stores/copilot/preview-store'
import { useCopilotStore } from '@/stores/copilot/store'
import type { CopilotMessage as CopilotMessageType } from '@/stores/copilot/types'
import CopilotMarkdownRenderer from './components/markdown-renderer'
import { ThinkingBlock } from './components/thinking-block'
const logger = createLogger('CopilotMessage')
@@ -574,7 +575,30 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
</div>
)
}
if (block.type === 'thinking') {
const isLastBlock = index === message.contentBlocks!.length - 1
// Consider the thinking block streaming if the overall message is streaming
// and the block has not been finalized with a duration yet. This avoids
// freezing the timer when new blocks are appended after the thinking block.
const isStreamingThinking = isStreaming && (block as any).duration == null
return (
<div key={`thinking-${index}-${block.timestamp || index}`} className='w-full'>
<ThinkingBlock
content={block.content}
isStreaming={isStreamingThinking}
duration={block.duration}
startTime={block.startTime}
/>
</div>
)
}
if (block.type === 'tool_call') {
// Skip hidden tools (like checkoff_todo)
if (block.toolCall.hidden) {
return null
}
return (
<div
key={`tool-${block.toolCall.id}`}
@@ -591,7 +615,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
if (isUser) {
return (
<div className='w-full py-2'>
<div className='w-full max-w-full overflow-hidden py-2'>
{/* File attachments displayed above the message, completely separate from message box width */}
{message.fileAttachments && message.fileAttachments.length > 0 && (
<div className='mb-1 flex justify-end'>
@@ -602,11 +626,14 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
)}
<div className='flex justify-end'>
<div className='max-w-[80%]'>
<div className='min-w-0 max-w-[80%]'>
{/* Message content in purple box */}
<div
className='rounded-[10px] px-3 py-2'
style={{ backgroundColor: 'rgba(128, 47, 255, 0.08)' }}
style={{
backgroundColor:
'color-mix(in srgb, var(--brand-primary-hover-hex) 8%, transparent)',
}}
>
<div className='whitespace-pre-wrap break-words font-normal text-base text-foreground leading-relaxed'>
<WordWrap text={message.content} />
@@ -728,9 +755,9 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
href={citation.url}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center rounded-md border bg-muted/50 px-2 py-1 text-muted-foreground text-xs transition-colors hover:bg-muted hover:text-foreground'
className='inline-flex max-w-full items-center rounded-md border bg-muted/50 px-2 py-1 text-muted-foreground text-xs transition-colors hover:bg-muted hover:text-foreground'
>
{citation.title}
<span className='truncate'>{citation.title}</span>
</a>
))}
</div>
@@ -760,7 +787,6 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
// For streaming messages, check if content actually changed
if (nextProps.isStreaming) {
// Compare contentBlocks length and lastUpdated for streaming messages
const prevBlocks = prevMessage.contentBlocks || []
const nextBlocks = nextMessage.contentBlocks || []
@@ -768,16 +794,37 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
return false // Content blocks changed
}
// Check if any text content changed in the last block
if (nextBlocks.length > 0) {
const prevLastBlock = prevBlocks[prevBlocks.length - 1]
const nextLastBlock = nextBlocks[nextBlocks.length - 1]
if (prevLastBlock?.type === 'text' && nextLastBlock?.type === 'text') {
if (prevLastBlock.content !== nextLastBlock.content) {
return false // Text content changed
// Helper: get last block content by type
const getLastBlockContent = (blocks: any[], type: 'text' | 'thinking'): string | null => {
for (let i = blocks.length - 1; i >= 0; i--) {
const block = blocks[i]
if (block && block.type === type) {
return (block as any).content ?? ''
}
}
return null
}
// Re-render if the last text block content changed
const prevLastTextContent = getLastBlockContent(prevBlocks as any[], 'text')
const nextLastTextContent = getLastBlockContent(nextBlocks as any[], 'text')
if (
prevLastTextContent !== null &&
nextLastTextContent !== null &&
prevLastTextContent !== nextLastTextContent
) {
return false
}
// Re-render if the last thinking block content changed
const prevLastThinkingContent = getLastBlockContent(prevBlocks as any[], 'thinking')
const nextLastThinkingContent = getLastBlockContent(nextBlocks as any[], 'thinking')
if (
prevLastThinkingContent !== null &&
nextLastThinkingContent !== null &&
prevLastThinkingContent !== nextLastThinkingContent
) {
return false
}
// Check if tool calls changed
@@ -788,14 +835,12 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
return false // Tool calls count changed
}
// Check if any tool call state changed
for (let i = 0; i < nextToolCalls.length; i++) {
if (prevToolCalls[i]?.state !== nextToolCalls[i]?.state) {
return false // Tool call state changed
}
}
// If we reach here, nothing meaningful changed during streaming
return true
}

View File

@@ -1,4 +1,5 @@
export * from './checkpoint-panel/checkpoint-panel'
export * from './copilot-message/copilot-message'
export * from './todo-list/todo-list'
export * from './user-input/user-input'
export * from './welcome/welcome'

Some files were not shown because too many files have changed in this diff Show More