mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 07:27:57 -05:00
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:
@@ -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}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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?',
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ?? {},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(''),
|
||||
|
||||
@@ -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'
|
||||
|
||||
210
apps/sim/app/api/workflows/yaml/export/route.ts
Normal file
210
apps/sim/app/api/workflows/yaml/export/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user