feat: light mode (#2457)

* feat(light): restored light theme

* feat: styling consolidation, note block upgrades

* improvement(zoom-prevention): moved downstream

* fix(notifications): mismatching workflow ID

* feat(light): button variant updates and controls consolidation

* improvement: UI consolidation

* feat: badges, usage limit; fix(note): iframe security; improvement(s-modal): sizing

* improvement: oauth modal, subscription

* improvement(team): ui/ux

* feat: emcn, subscription, tool input

* improvement(copilot): styling consolidation

* feat: colors consolidation

* improvement(ui): light styling

* fix(build): unused billing component

* improvement: addressed comments
This commit is contained in:
Emir Karabeg
2025-12-26 12:45:06 -08:00
committed by GitHub
parent 88cda3a9ce
commit 1f0e3f2be6
184 changed files with 4002 additions and 4472 deletions

View File

@@ -2,7 +2,7 @@
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { Loader2, X } from 'lucide-react' import { X } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
@@ -499,16 +499,11 @@ export default function CareersPage() {
className='min-w-[200px] rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all duration-300 hover:opacity-90 disabled:opacity-50' className='min-w-[200px] rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all duration-300 hover:opacity-90 disabled:opacity-50'
size='lg' size='lg'
> >
{isSubmitting ? ( {isSubmitting
<> ? 'Submitting...'
<Loader2 className='mr-2 h-4 w-4 animate-spin' /> : submitStatus === 'success'
Submitting... ? 'Submitted'
</> : 'Submit Application'}
) : submitStatus === 'success' ? (
'Submitted'
) : (
'Submit Application'
)}
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -1,5 +1,6 @@
'use client' 'use client'
import type { ComponentType, SVGProps } from 'react'
import { useState } from 'react' import { useState } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import type { LucideIcon } from 'lucide-react' import type { LucideIcon } from 'lucide-react'
@@ -24,7 +25,7 @@ import {
const logger = createLogger('LandingPricing') const logger = createLogger('LandingPricing')
interface PricingFeature { interface PricingFeature {
icon: LucideIcon icon: LucideIcon | ComponentType<SVGProps<SVGSVGElement>>
text: string text: string
} }

View File

@@ -7,7 +7,7 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const pathname = usePathname() const pathname = usePathname()
// Force light mode on public/marketing pages, dark mode everywhere else // Force light mode on public/marketing pages, allow user preference elsewhere
const isLightModePage = const isLightModePage =
pathname === '/' || pathname === '/' ||
pathname.startsWith('/login') || pathname.startsWith('/login') ||
@@ -27,10 +27,10 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
<NextThemesProvider <NextThemesProvider
attribute='class' attribute='class'
defaultTheme='dark' defaultTheme='dark'
enableSystem={false} enableSystem
disableTransitionOnChange disableTransitionOnChange
storageKey='sim-theme' storageKey='sim-theme'
forcedTheme={isLightModePage ? 'light' : 'dark'} forcedTheme={isLightModePage ? 'light' : undefined}
{...props} {...props}
> >
{children} {children}

View File

@@ -1,33 +0,0 @@
'use client'
import { useEffect } from 'react'
export function ZoomPrevention() {
useEffect(() => {
const preventZoom = (e: KeyboardEvent | WheelEvent) => {
// Prevent zoom on ctrl/cmd + wheel
if (e instanceof WheelEvent && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
}
// Prevent zoom on ctrl/cmd + plus/minus/zero
if (e instanceof KeyboardEvent && (e.ctrlKey || e.metaKey)) {
if (e.key === '=' || e.key === '-' || e.key === '0') {
e.preventDefault()
}
}
}
// Add event listeners
document.addEventListener('wheel', preventZoom, { passive: false })
document.addEventListener('keydown', preventZoom)
// Cleanup
return () => {
document.removeEventListener('wheel', preventZoom)
document.removeEventListener('keydown', preventZoom)
}
}, [])
return null
}

View File

@@ -8,7 +8,7 @@
*/ */
:root { :root {
--sidebar-width: 232px; --sidebar-width: 232px;
--panel-width: 244px; --panel-width: 260px;
--toolbar-triggers-height: 300px; --toolbar-triggers-height: 300px;
--editor-connections-height: 200px; --editor-connections-height: 200px;
--terminal-height: 196px; --terminal-height: 196px;
@@ -26,41 +26,6 @@
height: var(--terminal-height); height: var(--terminal-height);
} }
/**
* Workflow component z-index fixes and background colors
*/
.workflow-container .react-flow__edges {
z-index: 0 !important;
}
.workflow-container .react-flow__node {
z-index: 21 !important;
}
.workflow-container .react-flow__handle {
z-index: 30 !important;
}
.workflow-container .react-flow__edge [data-testid="workflow-edge"] {
z-index: 0 !important;
}
.workflow-container .react-flow__edge-labels {
z-index: 60 !important;
}
.workflow-container,
.workflow-container .react-flow__pane,
.workflow-container .react-flow__renderer {
background-color: var(--bg) !important;
}
.dark .workflow-container,
.dark .workflow-container .react-flow__pane,
.dark .workflow-container .react-flow__renderer {
background-color: var(--bg) !important;
}
/** /**
* Landing loop animation styles (keyframes defined in tailwind.config.ts) * Landing loop animation styles (keyframes defined in tailwind.config.ts)
*/ */
@@ -75,101 +40,87 @@
} }
/** /**
* Dark color tokens - single source of truth for all colors (dark-only) * Color tokens - single source of truth for all colors
* Light mode: Warm theme
* Dark mode: Dark neutral theme
*/ */
@layer base { @layer base {
:root, :root,
.light { .light {
/* Neutrals (surfaces) - shadcn stone palette */ --bg: #f9faf8; /* main canvas - near white */
--bg: #ffffff; /* pure white for landing/auth pages */ --surface-1: #f9faf8; /* sidebar, panels - light warm gray */
--surface-1: #fafaf9; /* stone-50 */ --surface-2: #fdfdfb; /* blocks, cards, modals - soft warm white */
--surface-2: #ffffff; /* white */ --surface-3: #f4f5f1; /* popovers, headers - more contrast */
--surface-3: #f5f5f4; /* stone-100 */ --surface-4: #f2f3ef; /* buttons base */
--surface-4: #f5f5f4; /* stone-100 */ --border: #d7dcda; /* primary border */
--surface-5: #eeedec; /* stone-150 */ --surface-5: #f0f1ed; /* inputs, form elements - subtle */
--surface-6: #f5f5f4; /* stone-100 */ --border-1: #d7dcda; /* stronger border - sage gray */
--surface-9: #f5f5f4; /* stone-100 */ --surface-6: #eceee9; /* popovers, elevated surfaces */
--surface-11: #e7e5e4; /* stone-200 */ --surface-7: #e8e9e4;
--surface-12: #d6d3d1; /* stone-300 */
--surface-13: #a8a29e; /* stone-400 */
--surface-14: #78716c; /* stone-500 */
--surface-15: #57534e; /* stone-600 */
--surface-elevated: #ffffff; /* white */
--bg-strong: #e7e5e4; /* stone-200 */
/* Text - shadcn stone palette for proper contrast */ --workflow-edge: #d7dcda; /* workflow handles/edges - matches border-1 */
--text-primary: #1c1917; /* stone-900 */
--text-secondary: #292524; /* stone-800 */
--text-tertiary: #57534e; /* stone-600 */
--text-muted: #78716c; /* stone-500 */
--text-subtle: #a8a29e; /* stone-400 */
--text-inverse: #fafaf9; /* stone-50 */
--text-error: #dc2626;
/* Borders / dividers - shadcn stone palette */ /* Text - warm neutrals */
--border: #d6d3d1; /* stone-300 */ --text-primary: #2d2d2d;
--border-strong: #d6d3d1; /* stone-300 */ --text-secondary: #404040;
--divider: #e7e5e4; /* stone-200 */ --text-tertiary: #5c5c5c;
--border-muted: #e7e5e4; /* stone-200 */ --text-muted: #737373;
--border-success: #d6d3d1; /* stone-300 */ --text-subtle: #8c8c8c;
--text-inverse: #f0fff6;
--text-error: #ef4444;
/* Borders / dividers */
--divider: #e8e9e4;
--border-muted: #dfe0db;
--border-success: #d7dcda;
/* Brand & state */ /* Brand & state */
--brand-400: #8e4cfb; --brand-400: #8e4cfb;
--brand-500: #6f3dfa;
--brand-secondary: #33b4ff; --brand-secondary: #33b4ff;
--brand-tertiary: #22c55e; --brand-tertiary: #22c55e;
--brand-tertiary-2: #33c481; --brand-tertiary-2: #32bd7e;
--warning: #ea580c; --warning: #ea580c;
/* Utility */ /* Utility */
--white: #ffffff; --white: #ffffff;
/* Font weights - lighter for light mode (-20 from dark) */ /* Font weights - lighter for light mode */
--font-weight-base: 430; --font-weight-base: 430;
--font-weight-medium: 450; --font-weight-medium: 450;
--font-weight-semibold: 500; --font-weight-semibold: 500;
/* RGB for opacity usage - stone palette */ /* Extended palette */
--surface-4-rgb: 245 245 244; /* stone-100 */ --c-0D0D0D: #0d0d0d;
--surface-5-rgb: 238 237 236; /* stone-150 */ --c-1A1A1A: #1a1a1a;
--surface-7-rgb: 245 245 244; /* stone-100 */ --c-1F1F1F: #1f1f1f;
--surface-9-rgb: 245 245 244; /* stone-100 */ --c-2A2A2A: #2a2a2a;
--divider-rgb: 231 229 228; /* stone-200 */ --c-383838: #383838;
--white-rgb: 255 255 255; --c-414141: #414141;
--black-rgb: 0 0 0;
/* Extended palette - mapped to shadcn stone palette */
--c-0D0D0D: #0c0a09; /* stone-950 */
--c-1A1A1A: #1c1917; /* stone-900 */
--c-1F1F1F: #1c1917; /* stone-900 */
--c-2A2A2A: #292524; /* stone-800 */
--c-383838: #44403c; /* stone-700 */
--c-414141: #57534e; /* stone-600 */
--c-442929: #442929; --c-442929: #442929;
--c-491515: #491515; --c-491515: #491515;
--c-575757: #78716c; /* stone-500 */ --c-575757: #575757;
--c-686868: #78716c; /* stone-500 */ --c-686868: #686868;
--c-707070: #78716c; /* stone-500 */ --c-707070: #707070;
--c-727272: #78716c; /* stone-500 */ --c-727272: #727272;
--c-737373: #78716c; /* stone-500 */ --c-737373: #737373;
--c-808080: #a8a29e; /* stone-400 */ --c-808080: #808080;
--c-858585: #a8a29e; /* stone-400 */ --c-858585: #858585;
--c-868686: #a8a29e; /* stone-400 */ --c-868686: #868686;
--c-8D8D8D: #a8a29e; /* stone-400 */ --c-8D8D8D: #8d8d8d;
--c-939393: #a8a29e; /* stone-400 */ --c-939393: #939393;
--c-A8A8A8: #a8a29e; /* stone-400 */ --c-A8A8A8: #a8a8a8;
--c-B8B8B8: #d6d3d1; /* stone-300 */ --c-B8B8B8: #b8b8b8;
--c-C0C0C0: #d6d3d1; /* stone-300 */ --c-C0C0C0: #c0c0c0;
--c-CDCDCD: #d6d3d1; /* stone-300 */ --c-CDCDCD: #cdcdcd;
--c-D0D0D0: #d6d3d1; /* stone-300 */ --c-D0D0D0: #d0d0d0;
--c-D2D2D2: #d6d3d1; /* stone-300 */ --c-D2D2D2: #d2d2d2;
--c-E0E0E0: #e7e5e4; /* stone-200 */ --c-E0E0E0: #e0e0e0;
--c-E5E5E5: #e7e5e4; /* stone-200 */ --c-E5E5E5: #e5e5e5;
--c-E8E8E8: #e7e5e4; /* stone-200 */ --c-E8E8E8: #e8e8e8;
--c-EEEEEE: #f5f5f4; /* stone-100 */ --c-EEEEEE: #eeeeee;
--c-F0F0F0: #f5f5f4; /* stone-100 */ --c-F0F0F0: #f0f0f0;
--c-F4F4F4: #fafaf9; /* stone-50 */ --c-F4F4F4: #f4f4f4;
--c-F5F5F5: #fafaf9; /* stone-50 */ --c-F5F5F5: #f5f5f5;
/* Blues and cyans */ /* Blues and cyans */
--c-00B0B0: #00b0b0; --c-00B0B0: #00b0b0;
@@ -203,30 +154,27 @@
/* Terminal status badges */ /* Terminal status badges */
--terminal-status-error-bg: #feeeee; --terminal-status-error-bg: #feeeee;
--terminal-status-error-border: #f87171; --terminal-status-error-border: #f87171;
--terminal-status-info-bg: #f5f5f4; /* stone-100 */ --terminal-status-info-bg: #f5f5f4;
--terminal-status-info-border: #a8a29e; /* stone-400 */ --terminal-status-info-border: #a8a29e;
--terminal-status-info-color: #57534e; /* stone-600 */ --terminal-status-info-color: #57534e;
--terminal-status-warning-bg: #fef9e7; --terminal-status-warning-bg: #fef9e7;
--terminal-status-warning-border: #f5c842; --terminal-status-warning-border: #f5c842;
--terminal-status-warning-color: #a16207; --terminal-status-warning-color: #a16207;
} }
.dark { .dark {
/* Neutrals (surfaces) */ /* Surface */
--bg: #1b1b1b; --bg: #1b1b1b;
--surface-1: #1e1e1e; --surface-1: #1e1e1e;
--surface-2: #232323; --surface-2: #232323;
--surface-3: #242424; --surface-3: #242424;
--surface-4: #252525; --surface-4: #292929;
--surface-5: #272727; --border: #2c2c2c;
--surface-6: #282828; --surface-5: #363636;
--surface-9: #363636; --border-1: #3d3d3d;
--surface-11: #3d3d3d; --surface-6: #454545;
--surface-12: #434343; --surface-7: #454545;
--surface-13: #454545;
--surface-14: #4a4a4a; --workflow-edge: #454545; /* workflow handles/edges - same as surface-6 in dark */
--surface-15: #5a5a5a;
--surface-elevated: #202020;
--bg-strong: #0c0c0c;
/* Text */ /* Text */
--text-primary: #e6e6e6; --text-primary: #e6e6e6;
@@ -237,9 +185,7 @@
--text-inverse: #1b1b1b; --text-inverse: #1b1b1b;
--text-error: #ef4444; --text-error: #ef4444;
/* Borders / dividers */ /* --border-strong: #303030; */
--border: #2c2c2c;
--border-strong: #303030;
--divider: #393939; --divider: #393939;
--border-muted: #424242; --border-muted: #424242;
--border-success: #575757; --border-success: #575757;
@@ -248,7 +194,7 @@
--brand-400: #8e4cfb; --brand-400: #8e4cfb;
--brand-secondary: #33b4ff; --brand-secondary: #33b4ff;
--brand-tertiary: #22c55e; --brand-tertiary: #22c55e;
--brand-tertiary-2: #33c481; --brand-tertiary-2: #32bd7e;
--warning: #ff6600; --warning: #ff6600;
/* Utility */ /* Utility */
@@ -259,15 +205,6 @@
--font-weight-medium: 480; --font-weight-medium: 480;
--font-weight-semibold: 550; --font-weight-semibold: 550;
/* RGB for opacity usage */
--surface-4-rgb: 37 37 37;
--surface-5-rgb: 39 39 39;
--surface-7-rgb: 44 44 44;
--surface-9-rgb: 54 54 54;
--divider-rgb: 57 57 57;
--white-rgb: 255 255 255;
--black-rgb: 0 0 0;
/* Extended palette (exhaustive from code usage via -[#...]) */ /* Extended palette (exhaustive from code usage via -[#...]) */
/* Neutral deep shades */ /* Neutral deep shades */
--c-0D0D0D: #0d0d0d; --c-0D0D0D: #0d0d0d;
@@ -395,34 +332,34 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: var(--surface-12); background-color: var(--surface-7);
border-radius: var(--radius); border-radius: var(--radius);
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background-color: var(--surface-13); background-color: var(--surface-7);
} }
/* Dark Mode Global Scrollbar */ /* Dark Mode Global Scrollbar */
.dark ::-webkit-scrollbar-track { .dark ::-webkit-scrollbar-track {
background: var(--surface-5); background: var(--surface-4);
} }
.dark ::-webkit-scrollbar-thumb { .dark ::-webkit-scrollbar-thumb {
background-color: var(--surface-12); background-color: var(--surface-7);
} }
.dark ::-webkit-scrollbar-thumb:hover { .dark ::-webkit-scrollbar-thumb:hover {
background-color: var(--surface-13); background-color: var(--surface-7);
} }
* { * {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--surface-12) var(--surface-1); scrollbar-color: var(--surface-7) var(--surface-1);
} }
.dark * { .dark * {
scrollbar-color: var(--surface-12) var(--surface-5); scrollbar-color: var(--surface-7) var(--surface-4);
} }
.copilot-scrollable { .copilot-scrollable {
@@ -438,8 +375,8 @@
} }
.panel-tab-active { .panel-tab-active {
background-color: var(--white); background-color: var(--surface-5);
color: var(--text-inverse); color: var(--text-primary);
border-color: var(--border-muted); border-color: var(--border-muted);
} }
@@ -450,7 +387,7 @@
} }
.panel-tab-inactive:hover { .panel-tab-inactive:hover {
background-color: var(--surface-9); background-color: var(--surface-5);
color: var(--text-primary); color: var(--text-primary);
} }
@@ -642,7 +579,7 @@ input[type="search"]::-ms-clear {
} }
html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="copilot"] { html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="copilot"] {
background-color: var(--surface-11) !important; background-color: var(--border-1) !important;
color: var(--text-primary) !important; color: var(--text-primary) !important;
} }
html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="toolbar"], html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="toolbar"],
@@ -652,7 +589,7 @@ input[type="search"]::-ms-clear {
} }
html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="toolbar"] { html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="toolbar"] {
background-color: var(--surface-11) !important; background-color: var(--border-1) !important;
color: var(--text-primary) !important; color: var(--text-primary) !important;
} }
html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="copilot"], html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="copilot"],
@@ -662,7 +599,7 @@ input[type="search"]::-ms-clear {
} }
html[data-panel-active-tab="editor"] .panel-container [data-tab-button="editor"] { html[data-panel-active-tab="editor"] .panel-container [data-tab-button="editor"] {
background-color: var(--surface-11) !important; background-color: var(--border-1) !important;
color: var(--text-primary) !important; color: var(--text-primary) !important;
} }
html[data-panel-active-tab="editor"] .panel-container [data-tab-button="copilot"], html[data-panel-active-tab="editor"] .panel-container [data-tab-button="copilot"],

View File

@@ -93,6 +93,7 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const context = searchParams.get('context') || 'user' const context = searchParams.get('context') || 'user'
const contextId = searchParams.get('id') const contextId = searchParams.get('id')
const includeOrg = searchParams.get('includeOrg') === 'true'
// Validate context parameter // Validate context parameter
if (!['user', 'organization'].includes(context)) { if (!['user', 'organization'].includes(context)) {
@@ -115,14 +116,38 @@ export async function GET(request: NextRequest) {
if (context === 'user') { if (context === 'user') {
// Get user billing (may include organization if they're part of one) // Get user billing (may include organization if they're part of one)
billingData = await getSimplifiedBillingSummary(session.user.id, contextId || undefined) billingData = await getSimplifiedBillingSummary(session.user.id, contextId || undefined)
// Attach effective billing blocked status (includes org owner check) // Attach effective billing blocked status (includes org owner check)
const billingStatus = await getEffectiveBillingStatus(session.user.id) const billingStatus = await getEffectiveBillingStatus(session.user.id)
billingData = { billingData = {
...billingData, ...billingData,
billingBlocked: billingStatus.billingBlocked, billingBlocked: billingStatus.billingBlocked,
billingBlockedReason: billingStatus.billingBlockedReason, billingBlockedReason: billingStatus.billingBlockedReason,
blockedByOrgOwner: billingStatus.blockedByOrgOwner, blockedByOrgOwner: billingStatus.blockedByOrgOwner,
} }
// Optionally include organization membership and role
if (includeOrg) {
const userMembership = await db
.select({
organizationId: member.organizationId,
role: member.role,
})
.from(member)
.where(eq(member.userId, session.user.id))
.limit(1)
if (userMembership.length > 0) {
billingData = {
...billingData,
organization: {
id: userMembership[0].organizationId,
role: userMembership[0].role as 'owner' | 'admin' | 'member',
},
}
}
}
} else { } else {
// Get user role in organization for permission checks first // Get user role in organization for permission checks first
const memberRecord = await db const memberRecord = await db

View File

@@ -2,7 +2,6 @@
import { type KeyboardEvent, useEffect, useState } from 'react' import { type KeyboardEvent, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp' import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
@@ -299,14 +298,7 @@ export default function EmailAuth({
className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`} className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`}
disabled={isSendingOtp} disabled={isSendingOtp}
> >
{isSendingOtp ? ( {isSendingOtp ? 'Sending Code...' : 'Continue'}
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Sending Code...
</>
) : (
'Continue'
)}
</Button> </Button>
</form> </form>
) : ( ) : (

View File

@@ -162,14 +162,7 @@ export function InviteStatusCard({
onClick={action.onClick} onClick={action.onClick}
disabled={action.disabled || action.loading} disabled={action.disabled || action.loading}
> >
{action.loading ? ( {action.loading ? `${action.label}...` : action.label}
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
{action.label}...
</>
) : (
action.label
)}
</Button> </Button>
))} ))}
</div> </div>

View File

@@ -11,7 +11,6 @@ import { HydrationErrorHandler } from '@/app/_shell/hydration-error-handler'
import { QueryProvider } from '@/app/_shell/providers/query-provider' import { QueryProvider } from '@/app/_shell/providers/query-provider'
import { SessionProvider } from '@/app/_shell/providers/session-provider' import { SessionProvider } from '@/app/_shell/providers/session-provider'
import { ThemeProvider } from '@/app/_shell/providers/theme-provider' import { ThemeProvider } from '@/app/_shell/providers/theme-provider'
import { ZoomPrevention } from '@/app/_shell/zoom-prevention'
import { season } from '@/app/_styles/fonts/season/season' import { season } from '@/app/_styles/fonts/season/season'
export const viewport: Viewport = { export const viewport: Viewport = {
@@ -85,7 +84,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
var panelWidth = panelState && panelState.panelWidth; var panelWidth = panelState && panelState.panelWidth;
var maxPanelWidth = window.innerWidth * 0.4; var maxPanelWidth = window.innerWidth * 0.4;
if (panelWidth >= 244 && panelWidth <= maxPanelWidth) { if (panelWidth >= 260 && panelWidth <= maxPanelWidth) {
document.documentElement.style.setProperty('--panel-width', panelWidth + 'px'); document.documentElement.style.setProperty('--panel-width', panelWidth + 'px');
} else if (panelWidth > maxPanelWidth) { } else if (panelWidth > maxPanelWidth) {
document.documentElement.style.setProperty('--panel-width', maxPanelWidth + 'px'); document.documentElement.style.setProperty('--panel-width', maxPanelWidth + 'px');
@@ -190,10 +189,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<ThemeProvider> <ThemeProvider>
<QueryProvider> <QueryProvider>
<SessionProvider> <SessionProvider>
<BrandedLayout> <BrandedLayout>{children}</BrandedLayout>
<ZoomPrevention />
{children}
</BrandedLayout>
</SessionProvider> </SessionProvider>
</QueryProvider> </QueryProvider>
</ThemeProvider> </ThemeProvider>

View File

@@ -317,10 +317,10 @@ export default function PlaygroundPage() {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</VariantRow> </VariantRow>
<VariantRow label='primary variant'> <VariantRow label='secondary variant'>
<Popover variant='primary'> <Popover variant='secondary'>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant='primary'>Primary Popover</Button> <Button variant='secondary'>Secondary Popover</Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> <PopoverContent>
<PopoverItem>Item 1</PopoverItem> <PopoverItem>Item 1</PopoverItem>
@@ -550,7 +550,7 @@ export default function PlaygroundPage() {
].map(({ Icon, name }) => ( ].map(({ Icon, name }) => (
<Tooltip.Root key={name}> <Tooltip.Root key={name}>
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<div className='flex h-10 w-10 cursor-pointer items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-5)]'> <div className='flex h-10 w-10 cursor-pointer items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-4)]'>
<Icon className='h-5 w-5 text-[var(--text-secondary)]' /> <Icon className='h-5 w-5 text-[var(--text-secondary)]' />
</div> </div>
</Tooltip.Trigger> </Tooltip.Trigger>

View File

@@ -732,7 +732,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
<> <>
{!currentUserId ? ( {!currentUserId ? (
<Button <Button
variant='primary' variant='tertiary'
onClick={() => { onClick={() => {
const callbackUrl = const callbackUrl =
isWorkspaceContext && workspaceId isWorkspaceContext && workspaceId
@@ -748,7 +748,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
</Button> </Button>
) : isWorkspaceContext ? ( ) : isWorkspaceContext ? (
<Button <Button
variant='primary' variant='tertiary'
onClick={handleUseTemplate} onClick={handleUseTemplate}
disabled={isUsing} disabled={isUsing}
className='!text-[#FFFFFF] h-[32px] rounded-[6px] px-[12px] text-[14px]' className='!text-[#FFFFFF] h-[32px] rounded-[6px] px-[12px] text-[14px]'
@@ -832,7 +832,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
/> />
</div> </div>
) : ( ) : (
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-elevated)]'> <div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-2)]'>
<User className='h-[14px] w-[14px] text-[var(--text-muted)]' /> <User className='h-[14px] w-[14px] text-[var(--text-muted)]' />
</div> </div>
)} )}
@@ -1001,7 +1001,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
/> />
</div> </div>
) : ( ) : (
<div className='flex h-[48px] w-[48px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-elevated)]'> <div className='flex h-[48px] w-[48px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-2)]'>
<User className='h-[24px] w-[24px] text-[var(--text-muted)]' /> <User className='h-[24px] w-[24px] text-[var(--text-muted)]' />
</div> </div>
)} )}

View File

@@ -27,15 +27,15 @@ interface TemplateCardProps {
export function TemplateCardSkeleton({ className }: { className?: string }) { export function TemplateCardSkeleton({ className }: { className?: string }) {
return ( return (
<div className={cn('h-[268px] w-full rounded-[8px] bg-[#202020] p-[8px]', className)}> <div className={cn('h-[268px] w-full rounded-[8px] bg-[var(--surface-3)] p-[8px]', className)}>
<div className='h-[180px] w-full animate-pulse rounded-[6px] bg-gray-700' /> <div className='h-[180px] w-full animate-pulse rounded-[6px] bg-[var(--surface-5)]' />
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='h-4 w-32 animate-pulse rounded bg-gray-700' /> <div className='h-4 w-32 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='flex items-center gap-[-4px]'> <div className='flex items-center gap-[-4px]'>
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 3 }).map((_, index) => (
<div <div
key={index} key={index}
className='h-[18px] w-[18px] animate-pulse rounded-[4px] bg-gray-700' className='h-[18px] w-[18px] animate-pulse rounded-[4px] bg-[var(--surface-5)]'
/> />
))} ))}
</div> </div>
@@ -43,14 +43,14 @@ export function TemplateCardSkeleton({ className }: { className?: string }) {
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='flex items-center gap-[6px]'> <div className='flex items-center gap-[6px]'>
<div className='h-[20px] w-[20px] animate-pulse rounded-full bg-gray-700' /> <div className='h-[20px] w-[20px] animate-pulse rounded-full bg-[var(--surface-5)]' />
<div className='h-3 w-20 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-20 animate-pulse rounded bg-[var(--surface-5)]' />
</div> </div>
<div className='flex items-center gap-[6px]'> <div className='flex items-center gap-[6px]'>
<div className='h-3 w-3 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-3 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-6 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-6 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-3 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-3 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-6 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-6 animate-pulse rounded bg-[var(--surface-5)]' />
</div> </div>
</div> </div>
</div> </div>
@@ -195,7 +195,7 @@ function TemplateCardInner({
return ( return (
<div <div
onClick={handleCardClick} onClick={handleCardClick}
className={cn('w-full cursor-pointer rounded-[8px] bg-[#202020] p-[8px]', className)} className={cn('w-full cursor-pointer rounded-[8px] bg-[var(--surface-3)] p-[8px]', className)}
> >
<div <div
ref={previewRef} ref={previewRef}
@@ -213,12 +213,14 @@ function TemplateCardInner({
lightweight lightweight
/> />
) : ( ) : (
<div className='h-full w-full bg-[#2A2A2A]' /> <div className='h-full w-full bg-[var(--surface-4)]' />
)} )}
</div> </div>
<div className='mt-[10px] flex items-center justify-between'> <div className='mt-[10px] flex items-center justify-between'>
<h3 className='truncate pr-[8px] pl-[2px] font-medium text-[16px] text-white'>{title}</h3> <h3 className='truncate pr-[8px] pl-[2px] font-medium text-[16px] text-[var(--text-primary)]'>
{title}
</h3>
<div className='flex flex-shrink-0'> <div className='flex flex-shrink-0'>
{blockTypes.length > 4 ? ( {blockTypes.length > 4 ? (
@@ -241,10 +243,12 @@ function TemplateCardInner({
) )
})} })}
<div <div
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[#4A4A4A]' className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--surface-7)]'
style={{ marginLeft: '-4px' }} style={{ marginLeft: '-4px' }}
> >
<span className='font-medium text-[10px] text-white'>+{blockTypes.length - 3}</span> <span className='font-medium text-[10px] text-[var(--text-primary)]'>
+{blockTypes.length - 3}
</span>
</div> </div>
</> </>
) : ( ) : (
@@ -276,24 +280,26 @@ function TemplateCardInner({
<img src={authorImageUrl} alt={author} className='h-full w-full object-cover' /> <img src={authorImageUrl} alt={author} className='h-full w-full object-cover' />
</div> </div>
) : ( ) : (
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-full bg-[#4A4A4A]'> <div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-7)]'>
<User className='h-[12px] w-[12px] text-[#888888]' /> <User className='h-[12px] w-[12px] text-[var(--text-muted)]' />
</div> </div>
)} )}
<div className='flex min-w-0 items-center gap-[4px]'> <div className='flex min-w-0 items-center gap-[4px]'>
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span> <span className='truncate font-medium text-[12px] text-[var(--text-muted)]'>
{author}
</span>
{isVerified && <VerifiedBadge size='sm' />} {isVerified && <VerifiedBadge size='sm' />}
</div> </div>
</div> </div>
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[#888888] text-[12px]'> <div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[12px] text-[var(--text-muted)]'>
<User className='h-[12px] w-[12px]' /> <User className='h-[12px] w-[12px]' />
<span>{usageCount}</span> <span>{usageCount}</span>
<Star <Star
onClick={handleStarClick} onClick={handleStarClick}
className={cn( className={cn(
'h-[12px] w-[12px] cursor-pointer transition-colors', 'h-[12px] w-[12px] cursor-pointer transition-colors',
isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[#888888]', isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[var(--text-muted)]',
isStarLoading && 'opacity-50' isStarLoading && 'opacity-50'
)} )}
/> />

View File

@@ -149,7 +149,7 @@ export default function Templates({
</div> </div>
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'> <div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' /> <Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input <Input
placeholder='Search' placeholder='Search'

View File

@@ -294,14 +294,14 @@ function UnsubscribeContent() {
variant='destructive' variant='destructive'
className='w-full' className='w-full'
> >
{processing ? ( {data?.currentPreferences.unsubscribeAll ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : data?.currentPreferences.unsubscribeAll ? (
<CheckCircle className='mr-2 h-4 w-4' /> <CheckCircle className='mr-2 h-4 w-4' />
) : null} ) : null}
{data?.currentPreferences.unsubscribeAll {processing
? 'Unsubscribed from All Emails' ? 'Unsubscribing...'
: 'Unsubscribe from All Marketing Emails'} : data?.currentPreferences.unsubscribeAll
? 'Unsubscribed from All Emails'
: 'Unsubscribe from All Marketing Emails'}
</Button> </Button>
<div className='text-center text-muted-foreground text-sm'> <div className='text-center text-muted-foreground text-sm'>

View File

@@ -2,7 +2,7 @@
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { AlertCircle, Loader2 } from 'lucide-react' import { AlertCircle } from 'lucide-react'
import { import {
Button, Button,
Label, Label,
@@ -157,19 +157,12 @@ export function CreateChunkModal({
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='tertiary'
onClick={handleCreateChunk} onClick={handleCreateChunk}
type='button' type='button'
disabled={!isFormValid || isCreating} disabled={!isFormValid || isCreating}
> >
{isCreating ? ( {isCreating ? 'Creating...' : 'Create Chunk'}
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Creating...
</>
) : (
'Create Chunk'
)}
</Button> </Button>
</ModalFooter> </ModalFooter>
</form> </form>
@@ -181,7 +174,7 @@ export function CreateChunkModal({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Discard Changes</ModalHeader> <ModalHeader>Discard Changes</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
You have unsaved changes. Are you sure you want to close without saving? You have unsaved changes. Are you sure you want to close without saving?
</p> </p>
</ModalBody> </ModalBody>
@@ -193,12 +186,7 @@ export function CreateChunkModal({
> >
Keep Editing Keep Editing
</Button> </Button>
<Button <Button variant='destructive' onClick={handleConfirmDiscard} type='button'>
variant='primary'
onClick={handleConfirmDiscard}
type='button'
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
Discard Changes Discard Changes
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -69,7 +69,7 @@ export function DeleteChunkModal({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Chunk</ModalHeader> <ModalHeader>Delete Chunk</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete this chunk?{' '} Are you sure you want to delete this chunk?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span> <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p> </p>
@@ -78,12 +78,7 @@ export function DeleteChunkModal({
<Button variant='active' disabled={isDeleting} onClick={onClose}> <Button variant='active' disabled={isDeleting} onClick={onClose}>
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={handleDeleteChunk} disabled={isDeleting}>
variant='primary'
onClick={handleDeleteChunk}
disabled={isDeleting}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{isDeleting ? <>Deleting...</> : <>Delete</>} {isDeleting ? <>Deleting...</> : <>Delete</>}
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { Loader2 } from 'lucide-react'
import { import {
Button, Button,
Combobox, Combobox,
@@ -575,7 +574,7 @@ export function DocumentTagsModal({
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='tertiary'
onClick={saveDocumentTag} onClick={saveDocumentTag}
className='flex-1' className='flex-1'
disabled={!canSaveTag} disabled={!canSaveTag}
@@ -741,7 +740,7 @@ export function DocumentTagsModal({
</Button> </Button>
)} )}
<Button <Button
variant='primary' variant='tertiary'
onClick={saveDocumentTag} onClick={saveDocumentTag}
className='flex-1' className='flex-1'
disabled={ disabled={
@@ -755,14 +754,7 @@ export function DocumentTagsModal({
)) ))
} }
> >
{isSavingTag ? ( {isSavingTag ? 'Creating...' : 'Create Tag'}
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Creating...
</>
) : (
'Create Tag'
)}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog' import * as DialogPrimitive from '@radix-ui/react-dialog'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { AlertCircle, ChevronDown, ChevronUp, Loader2, X } from 'lucide-react' import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'
import { import {
Button, Button,
Label, Label,
@@ -275,19 +275,12 @@ export function EditChunkModal({
</Button> </Button>
{userPermissions.canEdit && ( {userPermissions.canEdit && (
<Button <Button
variant='primary' variant='tertiary'
onClick={handleSaveContent} onClick={handleSaveContent}
type='button' type='button'
disabled={!isFormValid || isSaving || !hasUnsavedChanges || isNavigating} disabled={!isFormValid || isSaving || !hasUnsavedChanges || isNavigating}
> >
{isSaving ? ( {isSaving ? 'Saving...' : 'Save'}
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Saving...
</>
) : (
'Save'
)}
</Button> </Button>
)} )}
</ModalFooter> </ModalFooter>
@@ -300,7 +293,7 @@ export function EditChunkModal({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader> <ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
You have unsaved changes to this chunk content. You have unsaved changes to this chunk content.
{pendingNavigation {pendingNavigation
? ' Do you want to discard your changes and navigate to the next chunk?' ? ' Do you want to discard your changes and navigate to the next chunk?'
@@ -318,12 +311,7 @@ export function EditChunkModal({
> >
Keep Editing Keep Editing
</Button> </Button>
<Button <Button variant='destructive' onClick={handleConfirmDiscard} type='button'>
variant='primary'
onClick={handleConfirmDiscard}
type='button'
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
Discard Changes Discard Changes
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -234,7 +234,7 @@ function DocumentLoading({
</div> </div>
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'> <div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' /> <Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input <Input
placeholder='Search chunks...' placeholder='Search chunks...'
@@ -242,7 +242,7 @@ function DocumentLoading({
className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0' className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0'
/> />
</div> </div>
<Button disabled variant='primary' className='h-[32px] rounded-[6px]'> <Button disabled variant='tertiary' className='h-[32px] rounded-[6px]'>
Create Chunk Create Chunk
</Button> </Button>
</div> </div>
@@ -851,7 +851,7 @@ export function Document({
</div> </div>
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'> <div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' /> <Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input <Input
placeholder={ placeholder={
@@ -882,7 +882,7 @@ export function Document({
<Button <Button
onClick={() => setIsCreateChunkModalOpen(true)} onClick={() => setIsCreateChunkModalOpen(true)}
disabled={documentData?.processingStatus === 'failed' || !userPermissions.canEdit} disabled={documentData?.processingStatus === 'failed' || !userPermissions.canEdit}
variant='primary' variant='tertiary'
className='h-[32px] rounded-[6px]' className='h-[32px] rounded-[6px]'
> >
Create Chunk Create Chunk
@@ -1092,18 +1092,13 @@ export function Document({
)} )}
{documentData?.processingStatus === 'completed' && totalPages > 1 && ( {documentData?.processingStatus === 'completed' && totalPages > 1 && (
<div className='flex items-center justify-center border-t bg-background px-6 py-4'> <div className='flex items-center justify-center border-t bg-background px-4 pt-[10px]'>
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
<Button <Button variant='ghost' onClick={prevPage} disabled={!hasPrevPage}>
variant='ghost' <ChevronLeft className='h-3.5 w-3.5' />
onClick={prevPage}
disabled={!hasPrevPage}
className='h-8 w-8 p-0'
>
<ChevronLeft className='h-4 w-4' />
</Button> </Button>
<div className='mx-4 flex items-center gap-6'> <div className='mx-[12px] flex items-center gap-[16px]'>
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => { {Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let page: number let page: number
if (totalPages <= 5) { if (totalPages <= 5) {
@@ -1133,13 +1128,8 @@ export function Document({
})} })}
</div> </div>
<Button <Button variant='ghost' onClick={nextPage} disabled={!hasNextPage}>
variant='ghost' <ChevronRight className='h-3.5 w-3.5' />
onClick={nextPage}
disabled={!hasNextPage}
className='h-8 w-8 p-0'
>
<ChevronRight className='h-4 w-4' />
</Button> </Button>
</div> </div>
</div> </div>
@@ -1229,7 +1219,7 @@ export function Document({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Document</ModalHeader> <ModalHeader>Delete Document</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete "{effectiveDocumentName}"? This will permanently Are you sure you want to delete "{effectiveDocumentName}"? This will permanently
delete the document and all {documentData?.chunkCount ?? 0} chunk delete the document and all {documentData?.chunkCount ?? 0} chunk
{documentData?.chunkCount === 1 ? '' : 's'} within it.{' '} {documentData?.chunkCount === 1 ? '' : 's'} within it.{' '}
@@ -1245,10 +1235,9 @@ export function Document({
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='destructive'
onClick={handleDeleteDocument} onClick={handleDeleteDocument}
disabled={isDeletingDocument} disabled={isDeletingDocument}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
> >
{isDeletingDocument ? 'Deleting...' : 'Delete Document'} {isDeletingDocument ? 'Deleting...' : 'Delete Document'}
</Button> </Button>

View File

@@ -185,7 +185,7 @@ function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoadingProps)
</div> </div>
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'> <div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' /> <Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input <Input
placeholder='Search documents...' placeholder='Search documents...'
@@ -193,7 +193,7 @@ function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoadingProps)
className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0' className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0'
/> />
</div> </div>
<Button disabled variant='primary' className='h-[32px] rounded-[6px]'> <Button disabled variant='tertiary' className='h-[32px] rounded-[6px]'>
Add Documents Add Documents
</Button> </Button>
</div> </div>
@@ -973,7 +973,7 @@ export function KnowledgeBase({
</div> </div>
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'> <div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' /> <Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input <Input
placeholder='Search documents...' placeholder='Search documents...'
@@ -999,7 +999,7 @@ export function KnowledgeBase({
<Button <Button
onClick={handleAddDocuments} onClick={handleAddDocuments}
disabled={userPermissions.canEdit !== true} disabled={userPermissions.canEdit !== true}
variant='primary' variant='tertiary'
className='h-[32px] rounded-[6px]' className='h-[32px] rounded-[6px]'
> >
Add Documents Add Documents
@@ -1253,18 +1253,17 @@ export function KnowledgeBase({
)} )}
{totalPages > 1 && ( {totalPages > 1 && (
<div className='flex items-center justify-center border-t bg-background px-6 py-4'> <div className='flex items-center justify-center border-t bg-background px-4 pt-[10px]'>
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
<Button <Button
variant='ghost' variant='ghost'
onClick={prevPage} onClick={prevPage}
disabled={!hasPrevPage || isLoadingDocuments} disabled={!hasPrevPage || isLoadingDocuments}
className='h-8 w-8 p-0'
> >
<ChevronLeft className='h-4 w-4' /> <ChevronLeft className='h-3.5 w-3.5' />
</Button> </Button>
<div className='mx-4 flex items-center gap-6'> <div className='mx-[12px] flex items-center gap-[16px]'>
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => { {Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let page: number let page: number
if (totalPages <= 5) { if (totalPages <= 5) {
@@ -1298,9 +1297,8 @@ export function KnowledgeBase({
variant='ghost' variant='ghost'
onClick={nextPage} onClick={nextPage}
disabled={!hasNextPage || isLoadingDocuments} disabled={!hasNextPage || isLoadingDocuments}
className='h-8 w-8 p-0'
> >
<ChevronRight className='h-4 w-4' /> <ChevronRight className='h-3.5 w-3.5' />
</Button> </Button>
</div> </div>
</div> </div>
@@ -1315,7 +1313,7 @@ export function KnowledgeBase({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Knowledge Base</ModalHeader> <ModalHeader>Delete Knowledge Base</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete "{knowledgeBaseName}"? This will permanently delete Are you sure you want to delete "{knowledgeBaseName}"? This will permanently delete
the knowledge base and all {pagination.total} document the knowledge base and all {pagination.total} document
{pagination.total === 1 ? '' : 's'} within it.{' '} {pagination.total === 1 ? '' : 's'} within it.{' '}
@@ -1330,12 +1328,7 @@ export function KnowledgeBase({
> >
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={handleDeleteKnowledgeBase} disabled={isDeleting}>
variant='primary'
onClick={handleDeleteKnowledgeBase}
disabled={isDeleting}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{isDeleting ? 'Deleting...' : 'Delete Knowledge Base'} {isDeleting ? 'Deleting...' : 'Delete Knowledge Base'}
</Button> </Button>
</ModalFooter> </ModalFooter>
@@ -1346,7 +1339,7 @@ export function KnowledgeBase({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Document</ModalHeader> <ModalHeader>Delete Document</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete " Are you sure you want to delete "
{documents.find((doc) => doc.id === documentToDelete)?.filename ?? 'this document'}"?{' '} {documents.find((doc) => doc.id === documentToDelete)?.filename ?? 'this document'}"?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span> <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
@@ -1362,11 +1355,7 @@ export function KnowledgeBase({
> >
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={confirmDeleteDocument}>
variant='primary'
onClick={confirmDeleteDocument}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
Delete Document Delete Document
</Button> </Button>
</ModalFooter> </ModalFooter>
@@ -1377,7 +1366,7 @@ export function KnowledgeBase({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Documents</ModalHeader> <ModalHeader>Delete Documents</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete {selectedDocuments.size} document Are you sure you want to delete {selectedDocuments.size} document
{selectedDocuments.size === 1 ? '' : 's'}?{' '} {selectedDocuments.size === 1 ? '' : 's'}?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span> <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
@@ -1387,12 +1376,7 @@ export function KnowledgeBase({
<Button variant='active' onClick={() => setShowBulkDeleteModal(false)}> <Button variant='active' onClick={() => setShowBulkDeleteModal(false)}>
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={confirmBulkDelete} disabled={isBulkOperating}>
variant='primary'
onClick={confirmBulkDelete}
disabled={isBulkOperating}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{isBulkOperating {isBulkOperating
? 'Deleting...' ? 'Deleting...'
: `Delete ${selectedDocuments.size} Document${selectedDocuments.size === 1 ? '' : 's'}`} : `Delete ${selectedDocuments.size} Document${selectedDocuments.size === 1 ? '' : 's'}`}

View File

@@ -41,8 +41,8 @@ export function ActionBar({
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className={cn('-translate-x-1/2 fixed bottom-6 left-1/2 z-50 transform', className)} className={cn('-translate-x-1/2 fixed bottom-6 left-1/2 z-50 transform', className)}
> >
<div className='flex items-center gap-[8px] rounded-[10px] border border-[var(--border-strong)] bg-[var(--surface-1)] p-[8px]'> <div className='flex items-center gap-[8px] rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-[8px] py-[6px]'>
<span className='px-[4px] text-[13px] text-[var(--text-muted)]'> <span className='px-[4px] text-[13px] text-[var(--text-secondary)]'>
{selectedCount} selected {selectedCount} selected
</span> </span>
@@ -54,14 +54,12 @@ export function ActionBar({
variant='ghost' variant='ghost'
onClick={onEnable} onClick={onEnable}
disabled={isLoading} disabled={isLoading}
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]' className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]'
> >
<Circle className='h-[12px] w-[12px]' /> <Circle className='h-[12px] w-[12px]' />
</Button> </Button>
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content side='top'> <Tooltip.Content side='top'>Enable</Tooltip.Content>
Enable {disabledCount > 1 ? `${disabledCount} items` : 'item'}
</Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
)} )}
@@ -72,14 +70,12 @@ export function ActionBar({
variant='ghost' variant='ghost'
onClick={onDisable} onClick={onDisable}
disabled={isLoading} disabled={isLoading}
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]' className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]'
> >
<CircleOff className='h-[12px] w-[12px]' /> <CircleOff className='h-[12px] w-[12px]' />
</Button> </Button>
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content side='top'> <Tooltip.Content side='top'>Disable</Tooltip.Content>
Disable {enabledCount > 1 ? `${enabledCount} items` : 'item'}
</Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
)} )}
@@ -90,12 +86,12 @@ export function ActionBar({
variant='ghost' variant='ghost'
onClick={onDelete} onClick={onDelete}
disabled={isLoading} disabled={isLoading}
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]' className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]'
> >
<Trash2 className='h-[12px] w-[12px]' /> <Trash2 className='h-[12px] w-[12px]' />
</Button> </Button>
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content side='top'>Delete items</Tooltip.Content> <Tooltip.Content side='top'>Delete</Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
)} )}
</div> </div>

View File

@@ -352,7 +352,7 @@ export function AddDocumentsModal({
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='tertiary'
type='button' type='button'
onClick={handleUpload} onClick={handleUpload}
disabled={files.length === 0 || isUploading} disabled={files.length === 0 || isUploading}

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { Loader2 } from 'lucide-react'
import { import {
Button, Button,
Combobox, Combobox,
@@ -429,7 +428,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='tertiary'
onClick={saveTagDefinition} onClick={saveTagDefinition}
className='flex-1' className='flex-1'
disabled={ disabled={
@@ -438,14 +437,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
!hasAvailableSlots(createTagForm.fieldType) !hasAvailableSlots(createTagForm.fieldType)
} }
> >
{isSavingTag ? ( {isSavingTag ? 'Creating...' : 'Create Tag'}
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Creating...
</>
) : (
'Create Tag'
)}
</Button> </Button>
</div> </div>
</div> </div>
@@ -468,7 +460,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
<ModalHeader>Delete Tag</ModalHeader> <ModalHeader>Delete Tag</ModalHeader>
<ModalBody className='!pb-[16px]'> <ModalBody className='!pb-[16px]'>
<div className='space-y-[8px]'> <div className='space-y-[8px]'>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete the "{selectedTag?.displayName}" tag? This will Are you sure you want to delete the "{selectedTag?.displayName}" tag? This will
remove this tag from {selectedTagUsage?.documentCount || 0} document remove this tag from {selectedTagUsage?.documentCount || 0} document
{selectedTagUsage?.documentCount !== 1 ? 's' : ''}.{' '} {selectedTagUsage?.documentCount !== 1 ? 's' : ''}.{' '}
@@ -494,12 +486,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
> >
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={confirmDeleteTag} disabled={isDeletingTag}>
variant='primary'
onClick={confirmDeleteTag}
disabled={isDeletingTag}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{isDeletingTag ? <>Deleting...</> : 'Delete Tag'} {isDeletingTag ? <>Deleting...</> : 'Delete Tag'}
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -67,26 +67,26 @@ function formatAbsoluteDate(dateString: string): string {
*/ */
export function BaseCardSkeleton() { export function BaseCardSkeleton() {
return ( return (
<div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-elevated)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-5)]'> <div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-4)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-5)]'>
<div className='flex items-center justify-between gap-[8px]'> <div className='flex items-center justify-between gap-[8px]'>
<div className='h-[17px] w-[120px] animate-pulse rounded-[4px] bg-[var(--surface-9)]' /> <div className='h-[17px] w-[120px] animate-pulse rounded-[4px] bg-[var(--surface-5)]' />
<div className='h-[22px] w-[90px] animate-pulse rounded-[4px] bg-[var(--surface-5)]' /> <div className='h-[22px] w-[90px] animate-pulse rounded-[4px] bg-[var(--surface-4)]' />
</div> </div>
<div className='flex flex-1 flex-col gap-[8px]'> <div className='flex flex-1 flex-col gap-[8px]'>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<div className='flex items-center gap-[6px]'> <div className='flex items-center gap-[6px]'>
<div className='h-[12px] w-[12px] animate-pulse rounded-[2px] bg-[var(--surface-9)]' /> <div className='h-[12px] w-[12px] animate-pulse rounded-[2px] bg-[var(--surface-5)]' />
<div className='h-[15px] w-[45px] animate-pulse rounded-[4px] bg-[var(--surface-9)]' /> <div className='h-[15px] w-[45px] animate-pulse rounded-[4px] bg-[var(--surface-5)]' />
</div> </div>
<div className='h-[15px] w-[120px] animate-pulse rounded-[4px] bg-[var(--surface-5)]' /> <div className='h-[15px] w-[120px] animate-pulse rounded-[4px] bg-[var(--surface-4)]' />
</div> </div>
<div className='h-0 w-full border-[var(--divider)] border-t' /> <div className='h-0 w-full border-[var(--divider)] border-t' />
<div className='flex h-[36px] flex-col gap-[6px]'> <div className='flex h-[36px] flex-col gap-[6px]'>
<div className='h-[15px] w-full animate-pulse rounded-[4px] bg-[var(--surface-5)]' /> <div className='h-[15px] w-full animate-pulse rounded-[4px] bg-[var(--surface-4)]' />
<div className='h-[15px] w-[75%] animate-pulse rounded-[4px] bg-[var(--surface-5)]' /> <div className='h-[15px] w-[75%] animate-pulse rounded-[4px] bg-[var(--surface-4)]' />
</div> </div>
</div> </div>
</div> </div>
@@ -122,9 +122,9 @@ export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCa
return ( return (
<Link href={href} prefetch={true} className='h-full'> <Link href={href} prefetch={true} className='h-full'>
<div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-elevated)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-5)]'> <div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-4)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-5)]'>
<div className='flex items-center justify-between gap-[8px]'> <div className='flex items-center justify-between gap-[8px]'>
<h3 className='min-w-0 flex-1 truncate text-[14px] text-[var(--text-primary)]'> <h3 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
{title} {title}
</h3> </h3>
{shortId && <Badge className='flex-shrink-0 rounded-[4px] text-[12px]'>{shortId}</Badge>} {shortId && <Badge className='flex-shrink-0 rounded-[4px] text-[12px]'>{shortId}</Badge>}
@@ -139,7 +139,7 @@ export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCa
{updatedAt && ( {updatedAt && (
<Tooltip.Root> <Tooltip.Root>
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<span className='text-[12px] text-[var(--text-muted)]'> <span className='text-[12px] text-[var(--text-tertiary)]'>
last updated: {formatRelativeTime(updatedAt)} last updated: {formatRelativeTime(updatedAt)}
</span> </span>
</Tooltip.Trigger> </Tooltip.Trigger>

View File

@@ -1,8 +1,8 @@
export const filterButtonClass = export const filterButtonClass =
'w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[var(--white)] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]' 'w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[var(--white)] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-2)]'
export const dropdownContentClass = export const dropdownContentClass =
'w-[220px] rounded-lg border-[#E5E5E5] bg-[var(--white)] p-0 shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]' 'w-[220px] rounded-lg border-[#E5E5E5] bg-[var(--white)] p-0 shadow-xs dark:border-[#414141] dark:bg-[var(--surface-2)]'
export const commandListClass = 'overflow-y-auto overflow-x-hidden' export const commandListClass = 'overflow-y-auto overflow-x-hidden'

View File

@@ -388,7 +388,7 @@ export function CreateBaseModal({
/> />
</div> </div>
<div className='space-y-[12px] rounded-[6px] bg-[var(--surface-6)] px-[12px] py-[14px]'> <div className='space-y-[12px] rounded-[6px] bg-[var(--surface-5)] px-[12px] py-[14px]'>
<div className='grid grid-cols-2 gap-[12px]'> <div className='grid grid-cols-2 gap-[12px]'>
<div className='flex flex-col gap-[8px]'> <div className='flex flex-col gap-[8px]'>
<Label htmlFor='minChunkSize'>Min Chunk Size (characters)</Label> <Label htmlFor='minChunkSize'>Min Chunk Size (characters)</Label>
@@ -562,7 +562,7 @@ export function CreateBaseModal({
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='tertiary'
type='submit' type='submit'
disabled={isSubmitting || !nameValue?.trim()} disabled={isSubmitting || !nameValue?.trim()}
> >

View File

@@ -145,7 +145,7 @@ export function Knowledge() {
</div> </div>
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'> <div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' /> <Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input <Input
placeholder='Search' placeholder='Search'
@@ -184,7 +184,7 @@ export function Knowledge() {
<Button <Button
onClick={() => setIsCreateModalOpen(true)} onClick={() => setIsCreateModalOpen(true)}
disabled={userPermissions.canEdit !== true} disabled={userPermissions.canEdit !== true}
variant='primary' variant='tertiary'
className='h-[32px] rounded-[6px]' className='h-[32px] rounded-[6px]'
> >
Create Create

View File

@@ -671,7 +671,7 @@ function LineChartComponent({
const top = Math.min(Math.max(anchorY - 26, padding.top), height - padding.bottom - 18) const top = Math.min(Math.max(anchorY - 26, padding.top), height - padding.bottom - 18)
return ( return (
<div <div
className='pointer-events-none absolute rounded-[8px] border border-[var(--border-strong)] bg-[var(--surface-1)] px-[8px] py-[6px] font-medium text-[11px] shadow-lg' className='pointer-events-none absolute rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-1)] px-[8px] py-[6px] font-medium text-[11px] shadow-lg'
style={{ left, top }} style={{ left, top }}
> >
{currentHoverDate && ( {currentHoverDate && (

View File

@@ -97,7 +97,7 @@ export function StatusBar({
{hoverIndex !== null && segments[hoverIndex] && ( {hoverIndex !== null && segments[hoverIndex] && (
<div <div
className={`-translate-x-1/2 pointer-events-none absolute z-20 w-max whitespace-nowrap rounded-[8px] border border-[var(--border-strong)] bg-[var(--surface-1)] px-[8px] py-[6px] text-center text-[11px] shadow-lg ${ className={`-translate-x-1/2 pointer-events-none absolute z-20 w-max whitespace-nowrap rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-1)] px-[8px] py-[6px] text-center text-[11px] shadow-lg ${
preferBelow ? '' : '-translate-y-full' preferBelow ? '' : '-translate-y-full'
}`} }`}
style={{ style={{

View File

@@ -66,8 +66,8 @@ export function WorkflowsList({
<div <div
key={workflow.workflowId} key={workflow.workflowId}
className={cn( className={cn(
'flex h-[44px] cursor-pointer items-center gap-[16px] px-[24px] hover:bg-[var(--c-2A2A2A)]', 'flex h-[44px] cursor-pointer items-center gap-[16px] px-[24px] hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-4)]',
isSelected && 'bg-[var(--c-2A2A2A)]' isSelected && 'bg-[var(--surface-6)] dark:bg-[var(--surface-4)]'
)} )}
onClick={() => onToggleWorkflow(workflow.workflowId)} onClick={() => onToggleWorkflow(workflow.workflowId)}
> >

View File

@@ -36,7 +36,7 @@ const SKELETON_BAR_HEIGHTS = [
function GraphCardSkeleton({ title }: { title: string }) { function GraphCardSkeleton({ title }: { title: string }) {
return ( return (
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'> <div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-2)]'>
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'> <div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'> <span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
{title} {title}
@@ -570,7 +570,7 @@ export default function Dashboard({ logs, isLoading, error }: DashboardProps) {
<div className='mt-[24px] flex min-h-0 flex-1 flex-col pb-[24px]'> <div className='mt-[24px] flex min-h-0 flex-1 flex-col pb-[24px]'>
<div className='mb-[16px] flex-shrink-0'> <div className='mb-[16px] flex-shrink-0'>
<div className='grid grid-cols-1 gap-[16px] md:grid-cols-3'> <div className='grid grid-cols-1 gap-[16px] md:grid-cols-3'>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'> <div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-2)]'>
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'> <div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'> <span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
Runs Runs
@@ -597,7 +597,7 @@ export default function Dashboard({ logs, isLoading, error }: DashboardProps) {
</div> </div>
</div> </div>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'> <div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-2)]'>
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'> <div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'> <span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
Errors Errors
@@ -624,7 +624,7 @@ export default function Dashboard({ logs, isLoading, error }: DashboardProps) {
</div> </div>
</div> </div>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'> <div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-2)]'>
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'> <div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'> <span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
Latency Latency

View File

@@ -4,9 +4,4 @@ export { FileCards } from './log-details/components/file-download'
export { FrozenCanvas } from './log-details/components/frozen-canvas' export { FrozenCanvas } from './log-details/components/frozen-canvas'
export { TraceSpans } from './log-details/components/trace-spans' export { TraceSpans } from './log-details/components/trace-spans'
export { LogsList } from './logs-list' export { LogsList } from './logs-list'
export { export { AutocompleteSearch, LogsToolbar, NotificationSettings } from './logs-toolbar'
AutocompleteSearch,
Controls,
LogsToolbar,
NotificationSettings,
} from './logs-toolbar'

View File

@@ -277,10 +277,10 @@ function ExpandableRowHeader({
aria-expanded={hasChildren ? isExpanded : undefined} aria-expanded={hasChildren ? isExpanded : undefined}
aria-label={hasChildren ? (isExpanded ? 'Collapse' : 'Expand') : undefined} aria-label={hasChildren ? (isExpanded ? 'Collapse' : 'Expand') : undefined}
> >
<div className='flex items-center gap-[6px]'> <div className='flex items-center gap-[8px]'>
{hasChildren && ( {hasChildren && (
<ChevronDown <ChevronDown
className='h-[10px] w-[10px] text-[var(--text-subtle)] transition-transform group-hover:text-[var(--text-tertiary)]' className='h-[10px] w-[10px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]'
style={{ transform: isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' }} style={{ transform: isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' }}
/> />
)} )}
@@ -418,14 +418,14 @@ function InputOutputSection({
'font-medium text-[12px] transition-colors', 'font-medium text-[12px] transition-colors',
isError isError
? 'text-[var(--text-error)]' ? 'text-[var(--text-error)]'
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
)} )}
> >
{label} {label}
</span> </span>
<ChevronDown <ChevronDown
className={clsx( className={clsx(
'h-[10px] w-[10px] text-[var(--text-subtle)] transition-colors transition-transform group-hover:text-[var(--text-tertiary)]' 'h-[10px] w-[10px] text-[var(--text-tertiary)] transition-colors transition-transform group-hover:text-[var(--text-primary)]'
)} )}
style={{ style={{
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
@@ -502,7 +502,7 @@ function NestedBlockItem({
{/* Nested children */} {/* Nested children */}
{hasChildren && isChildrenExpanded && ( {hasChildren && isChildrenExpanded && (
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'> <div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l pl-[10px]'>
{span.children!.map((child, childIndex) => ( {span.children!.map((child, childIndex) => (
<NestedBlockItem <NestedBlockItem
key={child.id || `${spanId}-child-${childIndex}`} key={child.id || `${spanId}-child-${childIndex}`}
@@ -643,7 +643,7 @@ const TraceSpanItem = memo(function TraceSpanItem({
{/* For workflow blocks, keep children nested within the card (not as separate cards) */} {/* For workflow blocks, keep children nested within the card (not as separate cards) */}
{!isFirstSpan && isWorkflowBlock && inlineChildren.length > 0 && isCardExpanded && ( {!isFirstSpan && isWorkflowBlock && inlineChildren.length > 0 && isCardExpanded && (
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'> <div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l pl-[10px]'>
{inlineChildren.map((childSpan, index) => ( {inlineChildren.map((childSpan, index) => (
<NestedBlockItem <NestedBlockItem
key={childSpan.id || `${spanId}-nested-${index}`} key={childSpan.id || `${spanId}-nested-${index}`}
@@ -663,7 +663,7 @@ const TraceSpanItem = memo(function TraceSpanItem({
{/* For non-workflow blocks, render inline children/tool calls */} {/* For non-workflow blocks, render inline children/tool calls */}
{!isFirstSpan && !isWorkflowBlock && isCardExpanded && ( {!isFirstSpan && !isWorkflowBlock && isCardExpanded && (
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'> <div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l pl-[10px]'>
{[...toolCallSpans, ...inlineChildren].map((childSpan, index) => { {[...toolCallSpans, ...inlineChildren].map((childSpan, index) => {
const childId = childSpan.id || `${spanId}-inline-${index}` const childId = childSpan.id || `${spanId}-inline-${index}`
const childIsError = childSpan.status === 'error' const childIsError = childSpan.status === 'error'
@@ -731,7 +731,7 @@ const TraceSpanItem = memo(function TraceSpanItem({
{/* Nested children */} {/* Nested children */}
{showChildrenInProgressBar && hasNestedChildren && isNestedExpanded && ( {showChildrenInProgressBar && hasNestedChildren && isNestedExpanded && (
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'> <div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l pl-[10px]'>
{childSpan.children!.map((nestedChild, nestedIndex) => ( {childSpan.children!.map((nestedChild, nestedIndex) => (
<NestedBlockItem <NestedBlockItem
key={nestedChild.id || `${childId}-nested-${nestedIndex}`} key={nestedChild.id || `${childId}-nested-${nestedIndex}`}

View File

@@ -267,7 +267,7 @@ export const LogDetails = memo(function LogDetails({
</span> </span>
<button <button
onClick={() => setIsFrozenCanvasOpen(true)} onClick={() => setIsFrozenCanvasOpen(true)}
className='flex items-center justify-between rounded-[6px] bg-[var(--surface-1)] px-[10px] py-[8px] transition-colors hover:bg-[var(--c-2A2A2A)]' className='flex items-center justify-between rounded-[6px] bg-[var(--surface-1)] px-[10px] py-[8px] transition-colors hover:bg-[var(--surface-4)]'
> >
<span className='font-medium text-[12px] text-[var(--text-secondary)]'> <span className='font-medium text-[12px] text-[var(--text-secondary)]'>
View Snapshot View Snapshot

View File

@@ -38,8 +38,8 @@ const LogRow = memo(
<div <div
ref={isSelected ? selectedRowRef : null} ref={isSelected ? selectedRowRef : null}
className={cn( className={cn(
'relative flex h-[44px] cursor-pointer items-center px-[24px] hover:bg-[var(--c-2A2A2A)]', 'relative flex h-[44px] cursor-pointer items-center px-[24px] hover:bg-[var(--surface-4)]',
isSelected && 'bg-[var(--c-2A2A2A)]' isSelected && 'bg-[var(--surface-4)]'
)} )}
onClick={handleClick} onClick={handleClick}
> >

View File

@@ -1,263 +0,0 @@
import { type ReactNode, useState } from 'react'
import { ArrowUp, Bell, ChevronDown, Loader2, RefreshCw, Search } from 'lucide-react'
import {
Button,
Popover,
PopoverContent,
PopoverItem,
PopoverScrollArea,
PopoverTrigger,
Tooltip,
} from '@/components/emcn'
import { MoreHorizontal } from '@/components/emcn/icons'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/core/utils/cn'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { useFilterStore } from '@/stores/logs/filters/store'
import type { TimeRange } from '@/stores/logs/filters/types'
const FILTER_BUTTON_CLASS =
'w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[var(--white)] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
type TimelineProps = {
variant?: 'default' | 'header'
}
/**
* Timeline component for time range selection.
* Displays a dropdown with predefined time ranges.
* @param props - The component props
* @returns Time range selector dropdown
*/
function Timeline({ variant = 'default' }: TimelineProps = {}) {
const { timeRange, setTimeRange } = useFilterStore()
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const specificTimeRanges: TimeRange[] = [
'Past 30 minutes',
'Past hour',
'Past 6 hours',
'Past 12 hours',
'Past 24 hours',
'Past 3 days',
'Past 7 days',
'Past 14 days',
'Past 30 days',
]
const handleTimeRangeSelect = (range: TimeRange) => {
setTimeRange(range)
setIsPopoverOpen(false)
}
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button variant='outline' className={FILTER_BUTTON_CLASS}>
{timeRange}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
</Button>
</PopoverTrigger>
<PopoverContent
align={variant === 'header' ? 'end' : 'start'}
side='bottom'
sideOffset={4}
maxHeight={144}
>
<PopoverScrollArea>
<PopoverItem
active={timeRange === 'All time'}
showCheck
onClick={() => handleTimeRangeSelect('All time')}
>
All time
</PopoverItem>
<div className='my-[2px] h-px bg-[var(--surface-11)]' />
{specificTimeRanges.map((range) => (
<PopoverItem
key={range}
active={timeRange === range}
showCheck
onClick={() => handleTimeRangeSelect(range)}
>
{range}
</PopoverItem>
))}
</PopoverScrollArea>
</PopoverContent>
</Popover>
)
}
interface ControlsProps {
searchQuery?: string
setSearchQuery?: (v: string) => void
isRefetching: boolean
resetToNow: () => void
live: boolean
setLive: (v: (prev: boolean) => boolean) => void
viewMode: string
setViewMode: (mode: 'logs' | 'dashboard') => void
searchComponent?: ReactNode
showExport?: boolean
onExport?: () => void
canConfigureNotifications?: boolean
onConfigureNotifications?: () => void
}
export function Controls({
searchQuery,
setSearchQuery,
isRefetching,
resetToNow,
live,
setLive,
viewMode,
setViewMode,
searchComponent,
onExport,
canConfigureNotifications,
onConfigureNotifications,
}: ControlsProps) {
return (
<div
className={cn(
'mb-8 flex flex-col items-stretch justify-between gap-4 sm:flex-row sm:items-start',
soehne.className
)}
>
{searchComponent ? (
searchComponent
) : (
<div className='relative w-full max-w-md'>
<Search className='-translate-y-1/2 absolute top-1/2 left-3 h-[18px] w-[18px] text-muted-foreground' />
<Input
type='text'
placeholder='Search workflows...'
value={searchQuery}
onChange={(e) => setSearchQuery?.(e.target.value)}
className='h-9 w-full border-[#E5E5E5] bg-[var(--white)] pr-10 pl-9 dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
/>
{searchQuery && (
<button
onClick={() => setSearchQuery?.('')}
className='-translate-y-1/2 absolute top-1/2 right-3 text-muted-foreground hover:text-foreground'
>
<svg
width='14'
height='14'
viewBox='0 0 16 16'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
>
<path d='M12 4L4 12M4 4l8 8' />
</svg>
</button>
)}
</div>
)}
<div className='ml-auto flex flex-shrink-0 items-center gap-3'>
{viewMode !== 'dashboard' && (
<Popover>
<PopoverTrigger asChild>
<Button variant='ghost' className='h-9 w-9 p-0 hover:bg-secondary'>
<MoreHorizontal className='h-4 w-4' />
<span className='sr-only'>More options</span>
</Button>
</PopoverTrigger>
<PopoverContent align='end' sideOffset={4}>
<PopoverScrollArea>
<PopoverItem onClick={onExport}>
<ArrowUp className='h-3 w-3' />
<span>Export as CSV</span>
</PopoverItem>
<PopoverItem
onClick={canConfigureNotifications ? onConfigureNotifications : undefined}
disabled={!canConfigureNotifications}
>
<Bell className='h-3 w-3' />
<span>Configure Notifications</span>
</PopoverItem>
</PopoverScrollArea>
</PopoverContent>
</Popover>
)}
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
onClick={resetToNow}
className='h-9 w-9 p-0 hover:bg-secondary'
disabled={isRefetching}
>
{isRefetching ? (
<Loader2 className='h-4 w-4 animate-spin' />
) : (
<RefreshCw className='h-4 w-4' />
)}
<span className='sr-only'>Refresh</span>
</Button>
</Tooltip.Trigger>
<Tooltip.Content>{isRefetching ? 'Refreshing...' : 'Refresh'}</Tooltip.Content>
</Tooltip.Root>
<div className='inline-flex h-9 items-center rounded-[11px] border bg-card p-1 shadow-sm'>
<Button
variant='ghost'
onClick={() => setLive((v) => !v)}
className={cn(
'h-7 rounded-[8px] px-3 font-normal text-xs',
live
? 'bg-[var(--brand-primary-hex)] text-white shadow-[0_0_0_0_var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hover-hex)] hover:text-white hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
: 'text-muted-foreground hover:text-foreground'
)}
aria-pressed={live}
>
Live
</Button>
</div>
<div className='inline-flex h-9 items-center rounded-[11px] border bg-card p-1 shadow-sm'>
<Button
variant='ghost'
onClick={() => setViewMode('logs')}
className={cn(
'h-7 rounded-[8px] px-3 font-normal text-xs',
(viewMode as string) !== 'dashboard'
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
aria-pressed={(viewMode as string) !== 'dashboard'}
>
Logs
</Button>
<Button
variant='ghost'
onClick={() => setViewMode('dashboard')}
className={cn(
'h-7 rounded-[8px] px-3 font-normal text-xs',
(viewMode as string) === 'dashboard'
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
aria-pressed={(viewMode as string) === 'dashboard'}
>
Dashboard
</Button>
</div>
</div>
<div className='sm:hidden'>
<Timeline />
</div>
</div>
)
}
export default Controls

View File

@@ -1 +0,0 @@
export { Controls, default } from './controls'

View File

@@ -39,9 +39,6 @@ import { WorkflowSelector } from './components/workflow-selector'
const logger = createLogger('NotificationSettings') const logger = createLogger('NotificationSettings')
const PRIMARY_BUTTON_STYLES =
'!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
type NotificationType = 'webhook' | 'email' | 'slack' type NotificationType = 'webhook' | 'email' | 'slack'
type LogLevel = 'info' | 'error' type LogLevel = 'info' | 'error'
type AlertRule = type AlertRule =
@@ -618,10 +615,9 @@ export function NotificationSettings({
<div className='flex flex-shrink-0 items-center gap-[8px]'> <div className='flex flex-shrink-0 items-center gap-[8px]'>
<Button <Button
variant='primary' variant='tertiary'
onClick={() => handleTest(subscription.id)} onClick={() => handleTest(subscription.id)}
disabled={testNotification.isPending && testStatus?.id !== subscription.id} disabled={testNotification.isPending && testStatus?.id !== subscription.id}
className={PRIMARY_BUTTON_STYLES}
> >
{testStatus?.id === subscription.id {testStatus?.id === subscription.id
? testStatus.success ? testStatus.success
@@ -703,7 +699,7 @@ export function NotificationSettings({
{activeTab === 'email' && ( {activeTab === 'email' && (
<div className='flex flex-col gap-[8px]'> <div className='flex flex-col gap-[8px]'>
<Label className='text-[var(--text-secondary)]'>Email Recipients</Label> <Label className='text-[var(--text-secondary)]'>Email Recipients</Label>
<div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-9)]'> <div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-5)]'>
{invalidEmails.map((email, index) => ( {invalidEmails.map((email, index) => (
<EmailTag <EmailTag
key={`invalid-${index}`} key={`invalid-${index}`}
@@ -1302,10 +1298,9 @@ export function NotificationSettings({
</Button> </Button>
)} )}
<Button <Button
variant='primary' variant='tertiary'
onClick={handleSave} onClick={handleSave}
disabled={createNotification.isPending || updateNotification.isPending} disabled={createNotification.isPending || updateNotification.isPending}
className={PRIMARY_BUTTON_STYLES}
> >
{createNotification.isPending || updateNotification.isPending {createNotification.isPending || updateNotification.isPending
? editingId ? editingId
@@ -1322,9 +1317,8 @@ export function NotificationSettings({
resetForm() resetForm()
setShowForm(true) setShowForm(true)
}} }}
variant='primary' variant='tertiary'
disabled={isLoading} disabled={isLoading}
className={PRIMARY_BUTTON_STYLES}
> >
<Plus className='mr-[6px] h-[13px] w-[13px]' /> <Plus className='mr-[6px] h-[13px] w-[13px]' />
Add Add
@@ -1338,7 +1332,7 @@ export function NotificationSettings({
<ModalContent className='w-[400px]'> <ModalContent className='w-[400px]'>
<ModalHeader>Delete Notification</ModalHeader> <ModalHeader>Delete Notification</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
This will permanently remove the notification and stop all deliveries.{' '} This will permanently remove the notification and stop all deliveries.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span> <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p> </p>
@@ -1352,10 +1346,9 @@ export function NotificationSettings({
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='destructive'
onClick={handleDelete} onClick={handleDelete}
disabled={deleteNotification.isPending} disabled={deleteNotification.isPending}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
> >
{deleteNotification.isPending ? 'Deleting...' : 'Delete'} {deleteNotification.isPending ? 'Deleting...' : 'Delete'}
</Button> </Button>
@@ -1379,7 +1372,7 @@ function EmailTag({ email, onRemove, isInvalid }: EmailTagProps) {
'flex w-auto items-center gap-[4px] rounded-[4px] border px-[6px] py-[2px] text-[12px]', 'flex w-auto items-center gap-[4px] rounded-[4px] border px-[6px] py-[2px] text-[12px]',
isInvalid isInvalid
? 'border-[var(--text-error)] bg-[color-mix(in_srgb,var(--text-error)_10%,transparent)] text-[var(--text-error)] dark:bg-[color-mix(in_srgb,var(--text-error)_16%,transparent)]' ? 'border-[var(--text-error)] bg-[color-mix(in_srgb,var(--text-error)_10%,transparent)] text-[var(--text-error)] dark:bg-[color-mix(in_srgb,var(--text-error)_16%,transparent)]'
: 'border-[var(--surface-11)] bg-[var(--surface-5)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]' : 'border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
)} )}
> >
<span className='max-w-[200px] truncate'>{email}</span> <span className='max-w-[200px] truncate'>{email}</span>

View File

@@ -163,7 +163,7 @@ export function AutocompleteSearch({
}} }}
> >
<PopoverAnchor asChild> <PopoverAnchor asChild>
<div className='relative flex h-[32px] w-full items-center rounded-[8px] bg-[var(--surface-5)]'> <div className='relative flex h-[32px] w-[400px] items-center rounded-[8px] bg-[var(--surface-4)]'>
{/* Search Icon */} {/* Search Icon */}
<Search className='mr-[6px] ml-[8px] h-[14px] w-[14px] flex-shrink-0 text-[var(--text-subtle)]' /> <Search className='mr-[6px] ml-[8px] h-[14px] w-[14px] flex-shrink-0 text-[var(--text-subtle)]' />
@@ -266,8 +266,8 @@ export function AutocompleteSearch({
data-index={0} data-index={0}
className={cn( className={cn(
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]', 'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
'hover:bg-[var(--surface-9)]', 'hover:bg-[var(--surface-5)]',
highlightedIndex === 0 && 'bg-[var(--surface-9)]' highlightedIndex === 0 && 'bg-[var(--surface-5)]'
)} )}
onMouseEnter={() => setHighlightedIndex(0)} onMouseEnter={() => setHighlightedIndex(0)}
onMouseDown={(e) => { onMouseDown={(e) => {
@@ -296,8 +296,8 @@ export function AutocompleteSearch({
data-index={index} data-index={index}
className={cn( className={cn(
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]', 'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
'hover:bg-[var(--surface-9)]', 'hover:bg-[var(--surface-5)]',
isHighlighted && 'bg-[var(--surface-9)]' isHighlighted && 'bg-[var(--surface-5)]'
)} )}
onMouseEnter={() => setHighlightedIndex(index)} onMouseEnter={() => setHighlightedIndex(index)}
onMouseDown={(e) => { onMouseDown={(e) => {
@@ -339,8 +339,8 @@ export function AutocompleteSearch({
data-index={index} data-index={index}
className={cn( className={cn(
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]', 'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
'hover:bg-[var(--surface-9)]', 'hover:bg-[var(--surface-5)]',
index === highlightedIndex && 'bg-[var(--surface-9)]' index === highlightedIndex && 'bg-[var(--surface-5)]'
)} )}
onMouseEnter={() => setHighlightedIndex(index)} onMouseEnter={() => setHighlightedIndex(index)}
onMouseDown={(e) => { onMouseDown={(e) => {

View File

@@ -1,4 +1,3 @@
export { Controls } from './components/controls'
export { NotificationSettings } from './components/notifications' export { NotificationSettings } from './components/notifications'
export { AutocompleteSearch } from './components/search' export { AutocompleteSearch } from './components/search'
export { LogsToolbar } from './logs-toolbar' export { LogsToolbar } from './logs-toolbar'

View File

@@ -302,7 +302,7 @@ export function LogsToolbar({
</div> </div>
<div className='flex items-center gap-[8px]'> <div className='flex items-center gap-[8px]'>
{/* More options popover */} {/* More options popover */}
<Popover> <Popover size='sm'>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant='default' className='h-[32px] w-[32px] rounded-[6px] p-0'> <Button variant='default' className='h-[32px] w-[32px] rounded-[6px] p-0'>
<MoreHorizontal className='h-[14px] w-[14px]' /> <MoreHorizontal className='h-[14px] w-[14px]' />
@@ -339,27 +339,36 @@ export function LogsToolbar({
{/* Live button */} {/* Live button */}
<Button <Button
variant={isLive ? 'primary' : 'default'} variant={isLive ? 'tertiary' : 'default'}
onClick={onToggleLive} onClick={onToggleLive}
className='h-[32px] rounded-[6px] px-[10px]' className={cn(
'h-[32px] rounded-[6px] px-[10px]',
isLive && 'border border-[var(--brand-tertiary-2)]'
)}
> >
Live Live
</Button> </Button>
{/* View mode toggle */} {/* View mode toggle */}
<div <div
className='flex h-[32px] cursor-pointer items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-elevated)] p-[2px]' className='flex h-[32px] cursor-pointer items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-2)] p-[2px]'
onClick={() => onViewModeChange(isDashboardView ? 'logs' : 'dashboard')} onClick={() => onViewModeChange(isDashboardView ? 'logs' : 'dashboard')}
> >
<Button <Button
variant={!isDashboardView ? 'active' : 'ghost'} variant={!isDashboardView ? 'active' : 'ghost'}
className='h-[26px] rounded-[4px] px-[10px]' className={cn(
'h-[26px] rounded-[4px] px-[10px]',
isDashboardView && 'border border-transparent'
)}
> >
Logs Logs
</Button> </Button>
<Button <Button
variant={isDashboardView ? 'active' : 'ghost'} variant={isDashboardView ? 'active' : 'ghost'}
className='h-[26px] rounded-[4px] px-[10px]' className={cn(
'h-[26px] rounded-[4px] px-[10px]',
!isDashboardView && 'border border-transparent'
)}
> >
Dashboard Dashboard
</Button> </Button>

View File

@@ -5,10 +5,15 @@ import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
import { getBlock } from '@/blocks/registry' import { getBlock } from '@/blocks/registry'
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook'] as const const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook'] as const
const RUNNING_COLOR = '#22c55e' as const
const PENDING_COLOR = '#f59e0b' as const /** Possible execution status values for workflow logs */
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled' export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled'
/**
* Maps raw status string to LogStatus for display.
* @param status - Raw status from API
* @returns Normalized LogStatus value
*/
export function getDisplayStatus(status: string | null | undefined): LogStatus { export function getDisplayStatus(status: string | null | undefined): LogStatus {
switch (status) { switch (status) {
case 'running': case 'running':
@@ -24,108 +29,54 @@ export function getDisplayStatus(status: string | null | undefined): LogStatus {
} }
} }
/** /** Configuration mapping log status to Badge variant and display label */
* Checks if a hex color is gray/neutral (low saturation) or too light/dark const STATUS_VARIANT_MAP: Record<
*/ LogStatus,
export function isGrayOrNeutral(hex: string): boolean { { variant: React.ComponentProps<typeof Badge>['variant']; label: string }
const r = Number.parseInt(hex.slice(1, 3), 16) > = {
const g = Number.parseInt(hex.slice(3, 5), 16) error: { variant: 'red', label: 'Error' },
const b = Number.parseInt(hex.slice(5, 7), 16) pending: { variant: 'amber', label: 'Pending' },
running: { variant: 'green', label: 'Running' },
const max = Math.max(r, g, b) cancelled: { variant: 'gray', label: 'Cancelled' },
const min = Math.min(r, g, b) info: { variant: 'gray', label: 'Info' },
const lightness = (max + min) / 2 / 255
const delta = max - min
const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1)) / 255
return saturation < 0.2 || lightness > 0.8 || lightness < 0.25
} }
/** /** Configuration mapping core trigger types to Badge color variants */
* Converts a hex color to a background variant with appropriate opacity const TRIGGER_VARIANT_MAP: Record<string, React.ComponentProps<typeof Badge>['variant']> = {
*/ manual: 'gray-secondary',
export function hexToBackground(hex: string): string { api: 'blue',
const r = Number.parseInt(hex.slice(1, 3), 16) schedule: 'teal',
const g = Number.parseInt(hex.slice(3, 5), 16) chat: 'purple',
const b = Number.parseInt(hex.slice(5, 7), 16) webhook: 'orange',
return `rgba(${r}, ${g}, ${b}, 0.2)`
}
/**
* Lightens a hex color to make it more vibrant for text
*/
export function lightenColor(hex: string, percent = 30): string {
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)
const b = Number.parseInt(hex.slice(5, 7), 16)
const newR = Math.min(255, Math.round(r + (255 - r) * (percent / 100)))
const newG = Math.min(255, Math.round(g + (255 - g) * (percent / 100)))
const newB = Math.min(255, Math.round(b + (255 - b) * (percent / 100)))
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`
} }
interface StatusBadgeProps { interface StatusBadgeProps {
/** The execution status to display */
status: LogStatus status: LogStatus
} }
/** /**
* Displays a styled badge for a log execution status * Renders a colored badge indicating log execution status.
* @param props - Component props containing the status
* @returns A Badge with dot indicator and status label
*/ */
export const StatusBadge = React.memo(({ status }: StatusBadgeProps) => { export const StatusBadge = React.memo(({ status }: StatusBadgeProps) => {
const config = { const config = STATUS_VARIANT_MAP[status]
error: { return React.createElement(Badge, { variant: config.variant, dot: true }, config.label)
bg: 'var(--terminal-status-error-bg)',
color: 'var(--text-error)',
label: 'Error',
},
pending: {
bg: hexToBackground(PENDING_COLOR),
color: lightenColor(PENDING_COLOR, 65),
label: 'Pending',
},
running: {
bg: hexToBackground(RUNNING_COLOR),
color: lightenColor(RUNNING_COLOR, 65),
label: 'Running',
},
cancelled: {
bg: 'var(--terminal-status-info-bg)',
color: 'var(--terminal-status-info-color)',
label: 'Cancelled',
},
info: {
bg: 'var(--terminal-status-info-bg)',
color: 'var(--terminal-status-info-color)',
label: 'Info',
},
}[status]
return React.createElement(
'div',
{
className:
'inline-flex items-center gap-[6px] rounded-[6px] px-[9px] py-[2px] font-medium text-[12px]',
style: { backgroundColor: config.bg, color: config.color },
},
React.createElement('div', {
className: 'h-[6px] w-[6px] rounded-[2px]',
style: { backgroundColor: config.color },
}),
config.label
)
}) })
StatusBadge.displayName = 'StatusBadge' StatusBadge.displayName = 'StatusBadge'
interface TriggerBadgeProps { interface TriggerBadgeProps {
/** The trigger type identifier (e.g., 'manual', 'api', or integration block type) */
trigger: string trigger: string
} }
/** /**
* Displays a styled badge for a workflow trigger type * Renders a colored badge indicating the workflow trigger type.
* Core triggers display with their designated colors; integrations show with icons.
* @param props - Component props containing the trigger type
* @returns A Badge with appropriate styling for the trigger type
*/ */
export const TriggerBadge = React.memo(({ trigger }: TriggerBadgeProps) => { export const TriggerBadge = React.memo(({ trigger }: TriggerBadgeProps) => {
const metadata = getIntegrationMetadata(trigger) const metadata = getIntegrationMetadata(trigger)
@@ -133,37 +84,20 @@ export const TriggerBadge = React.memo(({ trigger }: TriggerBadgeProps) => {
const block = isIntegration ? getBlock(trigger) : null const block = isIntegration ? getBlock(trigger) : null
const IconComponent = block?.icon const IconComponent = block?.icon
const isUnknownIntegration = isIntegration && trigger !== 'generic' && !block const coreVariant = TRIGGER_VARIANT_MAP[trigger]
if ( if (coreVariant) {
trigger === 'manual' || return React.createElement(Badge, { variant: coreVariant }, metadata.label)
trigger === 'generic' || }
isUnknownIntegration ||
isGrayOrNeutral(metadata.color) if (IconComponent) {
) {
return React.createElement( return React.createElement(
Badge, Badge,
{ { variant: 'gray-secondary', icon: IconComponent },
variant: 'default',
className:
'inline-flex items-center gap-[6px] rounded-[6px] px-[9px] py-[2px] font-medium text-[12px]',
},
IconComponent && React.createElement(IconComponent, { className: 'h-[12px] w-[12px]' }),
metadata.label metadata.label
) )
} }
const textColor = lightenColor(metadata.color, 65) return React.createElement(Badge, { variant: 'gray-secondary' }, metadata.label)
return React.createElement(
'div',
{
className:
'inline-flex items-center gap-[6px] rounded-[6px] px-[9px] py-[2px] font-medium text-[12px]',
style: { backgroundColor: hexToBackground(metadata.color), color: textColor },
},
IconComponent && React.createElement(IconComponent, { className: 'h-[12px] w-[12px]' }),
metadata.label
)
}) })
TriggerBadge.displayName = 'TriggerBadge' TriggerBadge.displayName = 'TriggerBadge'

View File

@@ -29,19 +29,19 @@ export function TemplateCardSkeleton({ className }: { className?: string }) {
return ( return (
<div <div
className={cn( className={cn(
'h-[268px] w-full rounded-[8px] bg-[var(--surface-elevated)] p-[8px] transition-colors hover:bg-[var(--surface-5)]', 'h-[268px] w-full rounded-[8px] bg-[var(--surface-4)] p-[8px] transition-colors hover:bg-[var(--surface-5)]',
className className
)} )}
> >
<div className='h-[180px] w-full animate-pulse rounded-[6px] bg-gray-700' /> <div className='h-[180px] w-full animate-pulse rounded-[6px] bg-[var(--surface-5)]' />
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='h-4 w-32 animate-pulse rounded bg-gray-700' /> <div className='h-4 w-32 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='flex items-center gap-[-4px]'> <div className='flex items-center gap-[-4px]'>
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 3 }).map((_, index) => (
<div <div
key={index} key={index}
className='h-[18px] w-[18px] animate-pulse rounded-[4px] bg-gray-700' className='h-[18px] w-[18px] animate-pulse rounded-[4px] bg-[var(--surface-5)]'
/> />
))} ))}
</div> </div>
@@ -49,14 +49,14 @@ export function TemplateCardSkeleton({ className }: { className?: string }) {
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='flex items-center gap-[6px]'> <div className='flex items-center gap-[6px]'>
<div className='h-[20px] w-[20px] animate-pulse rounded-full bg-gray-700' /> <div className='h-[20px] w-[20px] animate-pulse rounded-full bg-[var(--surface-5)]' />
<div className='h-3 w-20 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-20 animate-pulse rounded bg-[var(--surface-5)]' />
</div> </div>
<div className='flex items-center gap-[6px]'> <div className='flex items-center gap-[6px]'>
<div className='h-3 w-3 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-3 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-6 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-6 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-3 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-3 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-6 animate-pulse rounded bg-gray-700' /> <div className='h-3 w-6 animate-pulse rounded bg-[var(--surface-5)]' />
</div> </div>
</div> </div>
</div> </div>
@@ -202,7 +202,7 @@ function TemplateCardInner({
<div <div
onClick={handleCardClick} onClick={handleCardClick}
className={cn( className={cn(
'w-full cursor-pointer rounded-[8px] bg-[var(--surface-elevated)] p-[8px] transition-colors hover:bg-[var(--surface-5)]', 'w-full cursor-pointer rounded-[8px] bg-[var(--surface-4)] p-[8px] transition-colors hover:bg-[var(--surface-5)]',
className className
)} )}
> >
@@ -223,12 +223,14 @@ function TemplateCardInner({
cursorStyle='pointer' cursorStyle='pointer'
/> />
) : ( ) : (
<div className='h-full w-full bg-[#2A2A2A]' /> <div className='h-full w-full bg-[var(--surface-4)]' />
)} )}
</div> </div>
<div className='mt-[10px] flex items-center justify-between'> <div className='mt-[10px] flex items-center justify-between'>
<h3 className='truncate pr-[8px] pl-[2px] font-medium text-[16px] text-white'>{title}</h3> <h3 className='truncate pr-[8px] pl-[2px] font-medium text-[16px] text-[var(--text-primary)]'>
{title}
</h3>
<div className='flex flex-shrink-0'> <div className='flex flex-shrink-0'>
{blockTypes.length > 4 ? ( {blockTypes.length > 4 ? (
@@ -251,10 +253,12 @@ function TemplateCardInner({
) )
})} })}
<div <div
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--surface-14)]' className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--surface-7)]'
style={{ marginLeft: '-4px' }} style={{ marginLeft: '-4px' }}
> >
<span className='font-medium text-[10px] text-white'>+{blockTypes.length - 3}</span> <span className='font-medium text-[10px] text-[var(--text-primary)]'>
+{blockTypes.length - 3}
</span>
</div> </div>
</> </>
) : ( ) : (
@@ -286,24 +290,26 @@ function TemplateCardInner({
<img src={authorImageUrl} alt={author} className='h-full w-full object-cover' /> <img src={authorImageUrl} alt={author} className='h-full w-full object-cover' />
</div> </div>
) : ( ) : (
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-14)]'> <div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-7)]'>
<User className='h-[12px] w-[12px] text-[#888888]' /> <User className='h-[12px] w-[12px] text-[var(--text-tertiary)]' />
</div> </div>
)} )}
<div className='flex min-w-0 items-center gap-[4px]'> <div className='flex min-w-0 items-center gap-[4px]'>
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span> <span className='truncate font-medium text-[12px] text-[var(--text-tertiary)]'>
{author}
</span>
{isVerified && <VerifiedBadge size='sm' />} {isVerified && <VerifiedBadge size='sm' />}
</div> </div>
</div> </div>
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[#888888] text-[12px]'> <div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[12px] text-[var(--text-tertiary)]'>
<User className='h-[12px] w-[12px]' /> <User className='h-[12px] w-[12px]' />
<span>{usageCount}</span> <span>{usageCount}</span>
<Star <Star
onClick={handleStarClick} onClick={handleStarClick}
className={cn( className={cn(
'h-[12px] w-[12px] cursor-pointer transition-colors', 'h-[12px] w-[12px] cursor-pointer transition-colors',
isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[#888888]', isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[var(--text-tertiary)]',
isStarLoading && 'opacity-50' isStarLoading && 'opacity-50'
)} )}
/> />

View File

@@ -186,7 +186,7 @@ export default function Templates({
</div> </div>
<div className='mt-[14px] flex items-center justify-between'> <div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'> <div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' /> <Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input <Input
placeholder='Search' placeholder='Search'

View File

@@ -37,7 +37,10 @@ import {
OutputSelect, OutputSelect,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components' } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components'
import { useChatFileUpload } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks' import { useChatFileUpload } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks'
import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import {
usePreventZoom,
useScrollManagement,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { import {
useFloatBoundarySync, useFloatBoundarySync,
useFloatDrag, useFloatDrag,
@@ -233,6 +236,7 @@ export function Chat() {
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null) const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const streamReaderRef = useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null) const streamReaderRef = useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null)
const preventZoomRef = usePreventZoom()
// File upload hook // File upload hook
const { const {
@@ -814,7 +818,8 @@ export function Chat() {
return ( return (
<div <div
className='fixed z-30 flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--surface-1)] px-[10px] pt-[2px] pb-[8px]' ref={preventZoomRef}
className='fixed z-30 flex flex-col overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--surface-1)] px-[10px] pt-[2px] pb-[8px]'
style={{ style={{
left: `${actualPosition.x}px`, left: `${actualPosition.x}px`,
top: `${actualPosition.y}px`, top: `${actualPosition.y}px`,
@@ -968,8 +973,8 @@ export function Chat() {
{/* Combined input container */} {/* Combined input container */}
<div <div
className={`rounded-[4px] border bg-[var(--surface-9)] py-0 pr-[6px] pl-[4px] transition-colors ${ className={`rounded-[4px] border bg-[var(--surface-5)] py-0 pr-[6px] pl-[4px] transition-colors ${
isDragOver ? 'border-[var(--brand-secondary)]' : 'border-[var(--surface-11)]' isDragOver ? 'border-[var(--brand-secondary)]' : 'border-[var(--border-1)]'
}`} }`}
> >
{/* File thumbnails */} {/* File thumbnails */}

View File

@@ -156,7 +156,7 @@ export function ChatMessage({ message }: ChatMessageProps) {
)} )}
{formattedContent && !formattedContent.startsWith('Uploaded') && ( {formattedContent && !formattedContent.startsWith('Uploaded') && (
<div className='rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-9)] px-[8px] py-[6px] transition-all duration-200'> <div className='rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] transition-all duration-200'>
<div className='whitespace-pre-wrap break-words font-medium font-sans text-gray-100 text-sm leading-[1.25rem]'> <div className='whitespace-pre-wrap break-words font-medium font-sans text-gray-100 text-sm leading-[1.25rem]'>
<WordWrap text={formattedContent} /> <WordWrap text={formattedContent} />
</div> </div>

View File

@@ -8,6 +8,7 @@ import { useParams, useRouter } from 'next/navigation'
import { Button } from '@/components/emcn' import { Button } from '@/components/emcn'
import { AgentIcon } from '@/components/icons' import { AgentIcon } from '@/components/icons'
import { cn } from '@/lib/core/utils/cn' import { cn } from '@/lib/core/utils/cn'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { useSearchModalStore } from '@/stores/search-modal/store' import { useSearchModalStore } from '@/stores/search-modal/store'
const logger = createLogger('WorkflowCommandList') const logger = createLogger('WorkflowCommandList')
@@ -58,6 +59,7 @@ export function CommandList() {
const params = useParams() const params = useParams()
const router = useRouter() const router = useRouter()
const { open: openSearchModal } = useSearchModalStore() const { open: openSearchModal } = useSearchModalStore()
const preventZoomRef = usePreventZoom()
const workspaceId = params.workspaceId as string | undefined const workspaceId = params.workspaceId as string | undefined
@@ -171,6 +173,7 @@ export function CommandList() {
return ( return (
<div <div
ref={preventZoomRef}
className={cn( className={cn(
'pointer-events-none absolute inset-0 mb-[50px] flex items-center justify-center' 'pointer-events-none absolute inset-0 mb-[50px] flex items-center justify-center'
)} )}

View File

@@ -3,6 +3,7 @@
import { memo, useMemo } from 'react' import { memo, useMemo } from 'react'
import { useViewport } from 'reactflow' import { useViewport } from 'reactflow'
import { useSession } from '@/lib/auth/auth-client' import { useSession } from '@/lib/auth/auth-client'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { getUserColor } from '@/app/workspace/[workspaceId]/w/utils/get-user-color' import { getUserColor } from '@/app/workspace/[workspaceId]/w/utils/get-user-color'
import { useSocket } from '@/app/workspace/providers/socket-provider' import { useSocket } from '@/app/workspace/providers/socket-provider'
@@ -23,6 +24,7 @@ const CursorsComponent = () => {
const viewport = useViewport() const viewport = useViewport()
const session = useSession() const session = useSession()
const currentUserId = session.data?.user?.id const currentUserId = session.data?.user?.id
const preventZoomRef = usePreventZoom()
const cursors = useMemo<CursorRenderData[]>(() => { const cursors = useMemo<CursorRenderData[]>(() => {
return presenceUsers return presenceUsers
@@ -41,7 +43,7 @@ const CursorsComponent = () => {
} }
return ( return (
<div className='pointer-events-none absolute inset-0 z-30 select-none'> <div ref={preventZoomRef} className='pointer-events-none absolute inset-0 z-30 select-none'>
{cursors.map(({ id, name, cursor, color }) => { {cursors.map(({ id, name, cursor, color }) => {
const x = cursor.x * viewport.zoom + viewport.x const x = cursor.x * viewport.zoom + viewport.x
const y = cursor.y * viewport.zoom + viewport.y const y = cursor.y * viewport.zoom + viewport.y

View File

@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
import clsx from 'clsx' import clsx from 'clsx'
import { Eye, EyeOff } from 'lucide-react' import { Eye, EyeOff } from 'lucide-react'
import { Button } from '@/components/emcn' import { Button } from '@/components/emcn'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { useCopilotStore } from '@/stores/panel/copilot/store' import { useCopilotStore } from '@/stores/panel/copilot/store'
import { useTerminalStore } from '@/stores/terminal' import { useTerminalStore } from '@/stores/terminal'
import { useWorkflowDiffStore } from '@/stores/workflow-diff' import { useWorkflowDiffStore } from '@/stores/workflow-diff'
@@ -295,6 +296,8 @@ export const DiffControls = memo(function DiffControls() {
}) })
}, [updatePreviewToolCallState, rejectChanges]) }, [updatePreviewToolCallState, rejectChanges])
const preventZoomRef = usePreventZoom()
// Don't show anything if no diff is available or diff is not ready // Don't show anything if no diff is available or diff is not ready
if (!hasActiveDiff || !isDiffReady) { if (!hasActiveDiff || !isDiffReady) {
return null return null
@@ -302,6 +305,7 @@ export const DiffControls = memo(function DiffControls() {
return ( return (
<div <div
ref={preventZoomRef}
className={clsx( className={clsx(
'-translate-x-1/2 fixed left-1/2 z-30', '-translate-x-1/2 fixed left-1/2 z-30',
!isTerminalResizing && 'transition-[bottom] duration-100 ease-out' !isTerminalResizing && 'transition-[bottom] duration-100 ease-out'
@@ -335,9 +339,9 @@ export const DiffControls = memo(function DiffControls() {
{/* Accept */} {/* Accept */}
<Button <Button
variant='ghost' variant='tertiary'
onClick={handleAccept} onClick={handleAccept}
className='!text-[var(--bg)] h-[30px] rounded-[8px] bg-[var(--brand-tertiary)] px-3' className='h-[30px] rounded-[8px] px-3'
title='Accept changes' title='Accept changes'
> >
Accept Accept

View File

@@ -4,6 +4,7 @@ import { Component, type ReactNode, useEffect } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { ReactFlowProvider } from 'reactflow' import { ReactFlowProvider } from 'reactflow'
import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components' import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar' import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
const logger = createLogger('ErrorBoundary') const logger = createLogger('ErrorBoundary')
@@ -24,12 +25,13 @@ export function ErrorUI({
onReset, onReset,
fullScreen = false, fullScreen = false,
}: ErrorUIProps) { }: ErrorUIProps) {
const preventZoomRef = usePreventZoom()
const containerClass = fullScreen const containerClass = fullScreen
? 'flex flex-col w-full h-screen bg-[var(--surface-1)]' ? 'flex flex-col w-full h-screen bg-[var(--surface-1)]'
: 'flex flex-col w-full h-full bg-[var(--surface-1)]' : 'flex flex-col w-full h-full bg-[var(--surface-1)]'
return ( return (
<div className={containerClass}> <div ref={preventZoomRef} className={containerClass}>
{/* Sidebar */} {/* Sidebar */}
<Sidebar /> <Sidebar />

View File

@@ -4,7 +4,6 @@ export { DiffControls } from './diff-controls/diff-controls'
export { ErrorBoundary } from './error/index' export { ErrorBoundary } from './error/index'
export { Notifications } from './notifications/notifications' export { Notifications } from './notifications/notifications'
export { Panel } from './panel/panel' export { Panel } from './panel/panel'
export { SkeletonLoading } from './skeleton-loading/skeleton-loading'
export { SubflowNodeComponent } from './subflows/subflow-node' export { SubflowNodeComponent } from './subflows/subflow-node'
export { Terminal } from './terminal/terminal' export { Terminal } from './terminal/terminal'
export { WandPromptBar } from './wand-prompt-bar/wand-prompt-bar' export { WandPromptBar } from './wand-prompt-bar/wand-prompt-bar'

View File

@@ -27,6 +27,21 @@ function extractFieldValue(rawValue: unknown): string | undefined {
return undefined return undefined
} }
/**
* Extract YouTube video ID from various YouTube URL formats
*/
function getYouTubeVideoId(url: string): string | null {
const patterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
/youtube\.com\/watch\?.*v=([a-zA-Z0-9_-]{11})/,
]
for (const pattern of patterns) {
const match = url.match(pattern)
if (match) return match[1]
}
return null
}
/** /**
* Compact markdown renderer for note blocks with tight spacing * Compact markdown renderer for note blocks with tight spacing
*/ */
@@ -36,39 +51,41 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
components={{ components={{
p: ({ children }: any) => ( p: ({ children }: any) => (
<p className='mb-2 break-words text-[#E5E5E5] text-sm'>{children}</p> <p className='mb-1 break-words text-[#E5E5E5] text-sm leading-[1.25rem] last:mb-0'>
{children}
</p>
), ),
h1: ({ children }: any) => ( h1: ({ children }: any) => (
<h1 className='mt-3 mb-1 break-words font-semibold text-[#E5E5E5] text-lg first:mt-0'> <h1 className='mt-3 mb-3 break-words font-semibold text-[#E5E5E5] text-lg first:mt-0'>
{children} {children}
</h1> </h1>
), ),
h2: ({ children }: any) => ( h2: ({ children }: any) => (
<h2 className='mt-3 mb-1 break-words font-semibold text-[#E5E5E5] text-base first:mt-0'> <h2 className='mt-2.5 mb-2.5 break-words font-semibold text-[#E5E5E5] text-base first:mt-0'>
{children} {children}
</h2> </h2>
), ),
h3: ({ children }: any) => ( h3: ({ children }: any) => (
<h3 className='mt-3 mb-1 break-words font-semibold text-[#E5E5E5] text-sm first:mt-0'> <h3 className='mt-2 mb-2 break-words font-semibold text-[#E5E5E5] text-sm first:mt-0'>
{children} {children}
</h3> </h3>
), ),
h4: ({ children }: any) => ( h4: ({ children }: any) => (
<h4 className='mt-3 mb-1 break-words font-semibold text-[#E5E5E5] text-xs first:mt-0'> <h4 className='mt-2 mb-2 break-words font-semibold text-[#E5E5E5] text-xs first:mt-0'>
{children} {children}
</h4> </h4>
), ),
ul: ({ children }: any) => ( ul: ({ children }: any) => (
<ul className='mt-1 mb-2 list-disc break-words pl-4 text-[#E5E5E5] text-sm'> <ul className='mt-1 mb-1 list-disc space-y-1 break-words pl-6 text-[#E5E5E5] text-sm'>
{children} {children}
</ul> </ul>
), ),
ol: ({ children }: any) => ( ol: ({ children }: any) => (
<ol className='mt-1 mb-2 list-decimal break-words pl-4 text-[#E5E5E5] text-sm'> <ol className='mt-1 mb-1 list-decimal space-y-1 break-words pl-6 text-[#E5E5E5] text-sm'>
{children} {children}
</ol> </ol>
), ),
li: ({ children }: any) => <li className='mb-0 break-words'>{children}</li>, li: ({ children }: any) => <li className='break-words'>{children}</li>,
code: ({ inline, className, children, ...props }: any) => { code: ({ inline, className, children, ...props }: any) => {
const isInline = inline || !className?.includes('language-') const isInline = inline || !className?.includes('language-')
@@ -76,7 +93,7 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
return ( return (
<code <code
{...props} {...props}
className='whitespace-normal rounded bg-gray-200 px-1 py-0.5 font-mono text-[#F59E0B] text-xs dark:bg-[var(--surface-11)]' className='whitespace-normal rounded bg-gray-200 px-1 py-0.5 font-mono text-[#F59E0B] text-xs dark:bg-[var(--border-1)]'
> >
{children} {children}
</code> </code>
@@ -92,22 +109,51 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
</code> </code>
) )
}, },
a: ({ href, children }: any) => ( a: ({ href, children }: any) => {
<a const videoId = href ? getYouTubeVideoId(href) : null
href={href} if (videoId) {
target='_blank' return (
rel='noopener noreferrer' <span className='inline'>
className='text-[var(--brand-secondary)] underline-offset-2 hover:underline' <a
> href={href}
{children} target='_blank'
</a> rel='noopener noreferrer'
), className='text-[var(--brand-secondary)] underline-offset-2 hover:underline'
>
{children}
</a>
<span className='mt-1.5 block overflow-hidden rounded-md'>
<iframe
src={`https://www.youtube.com/embed/${videoId}`}
title='YouTube video'
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
loading='lazy'
referrerPolicy='strict-origin-when-cross-origin'
sandbox='allow-scripts allow-same-origin allow-presentation allow-popups'
className='aspect-video w-full'
/>
</span>
</span>
)
}
return (
<a
href={href}
target='_blank'
rel='noopener noreferrer'
className='text-[var(--brand-secondary)] underline-offset-2 hover:underline'
>
{children}
</a>
)
},
strong: ({ children }: any) => ( strong: ({ children }: any) => (
<strong className='break-words font-semibold text-white'>{children}</strong> <strong className='break-words font-semibold text-white'>{children}</strong>
), ),
em: ({ children }: any) => <em className='break-words text-[#B8B8B8]'>{children}</em>, em: ({ children }: any) => <em className='break-words text-[#B8B8B8]'>{children}</em>,
blockquote: ({ children }: any) => ( blockquote: ({ children }: any) => (
<blockquote className='mt-1 mb-2 break-words border-[#F59E0B] border-l-2 pl-3 text-[#B8B8B8] italic'> <blockquote className='my-4 break-words border-[var(--border-1)] border-l-4 py-1 pl-4 text-[#B8B8B8] italic'>
{children} {children}
</blockquote> </blockquote>
), ),
@@ -135,28 +181,16 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
) )
) )
const noteValues = useMemo(() => { const content = useMemo(() => {
if (data.isPreview && data.subBlockValues) { if (data.isPreview && data.subBlockValues) {
const extractedPreviewFormat = extractFieldValue(data.subBlockValues.format) const extractedContent = extractFieldValue(data.subBlockValues.content)
const extractedPreviewContent = extractFieldValue(data.subBlockValues.content) return typeof extractedContent === 'string' ? extractedContent : ''
return {
format: typeof extractedPreviewFormat === 'string' ? extractedPreviewFormat : 'plain',
content: typeof extractedPreviewContent === 'string' ? extractedPreviewContent : '',
}
}
const format = extractFieldValue(storedValues?.format)
const content = extractFieldValue(storedValues?.content)
return {
format: typeof format === 'string' ? format : 'plain',
content: typeof content === 'string' ? content : '',
} }
const storedContent = extractFieldValue(storedValues?.content)
return typeof storedContent === 'string' ? storedContent : ''
}, [data.isPreview, data.subBlockValues, storedValues]) }, [data.isPreview, data.subBlockValues, storedValues])
const content = noteValues.content ?? ''
const isEmpty = content.trim().length === 0 const isEmpty = content.trim().length === 0
const showMarkdown = noteValues.format === 'markdown' && !isEmpty
const userPermissions = useUserPermissionsContext() const userPermissions = useUserPermissionsContext()
@@ -182,7 +216,7 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
<div className='group relative'> <div className='group relative'>
<div <div
className={cn( className={cn(
'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] bg-[var(--surface-2)]' 'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] border border-[var(--border)] bg-[var(--surface-2)]'
)} )}
onClick={handleClick} onClick={handleClick}
> >
@@ -194,15 +228,12 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
event.stopPropagation() event.stopPropagation()
}} }}
> >
<div className='flex min-w-0 flex-1 items-center gap-[10px]'> <div className='flex min-w-0 flex-1 items-center'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ background: isEnabled ? config.bgColor : 'gray' }}
>
<config.icon className='h-[16px] w-[16px] text-white' />
</div>
<span <span
className={cn('font-medium text-[16px]', !isEnabled && 'truncate text-[#808080]')} className={cn(
'truncate font-medium text-[16px]',
!isEnabled && 'text-[var(--text-muted)]'
)}
title={name} title={name}
> >
{name} {name}
@@ -210,14 +241,12 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
</div> </div>
</div> </div>
<div className='relative px-[12px] pt-[6px] pb-[8px]'> <div className='relative p-[8px]'>
<div className='relative break-words'> <div className='relative break-words'>
{isEmpty ? ( {isEmpty ? (
<p className='text-[#868686] text-sm italic'>Add a note...</p> <p className='text-[#868686] text-sm'>Add note...</p>
) : showMarkdown ? (
<NoteMarkdown content={content} />
) : ( ) : (
<p className='whitespace-pre-wrap text-[#E5E5E5] text-sm leading-snug'>{content}</p> <NoteMarkdown content={content} />
)} )}
</div> </div>
</div> </div>

View File

@@ -1,17 +1,18 @@
import { memo, useCallback } from 'react' import { memo, useCallback, useMemo } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import clsx from 'clsx' import clsx from 'clsx'
import { X } from 'lucide-react' import { X } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button } from '@/components/emcn' import { Button } from '@/components/emcn'
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { import {
type NotificationAction, type NotificationAction,
openCopilotWithMessage, openCopilotWithMessage,
useNotificationStore, useNotificationStore,
} from '@/stores/notifications' } from '@/stores/notifications'
import { useTerminalStore } from '@/stores/terminal' import { useTerminalStore } from '@/stores/terminal'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const logger = createLogger('Notifications') const logger = createLogger('Notifications')
const MAX_VISIBLE_NOTIFICATIONS = 4 const MAX_VISIBLE_NOTIFICATIONS = 4
@@ -22,15 +23,18 @@ const MAX_VISIBLE_NOTIFICATIONS = 4
* Shows both global notifications and workflow-specific notifications * Shows both global notifications and workflow-specific notifications
*/ */
export const Notifications = memo(function Notifications() { export const Notifications = memo(function Notifications() {
const params = useParams() const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
const workflowId = params.workflowId as string
const notifications = useNotificationStore((state) => const allNotifications = useNotificationStore((state) => state.notifications)
state.notifications.filter((n) => !n.workflowId || n.workflowId === workflowId)
)
const removeNotification = useNotificationStore((state) => state.removeNotification) const removeNotification = useNotificationStore((state) => state.removeNotification)
const clearNotifications = useNotificationStore((state) => state.clearNotifications) const clearNotifications = useNotificationStore((state) => state.clearNotifications)
const visibleNotifications = notifications.slice(0, MAX_VISIBLE_NOTIFICATIONS)
const visibleNotifications = useMemo(() => {
if (!activeWorkflowId) return []
return allNotifications
.filter((n) => !n.workflowId || n.workflowId === activeWorkflowId)
.slice(0, MAX_VISIBLE_NOTIFICATIONS)
}, [allNotifications, activeWorkflowId])
const isTerminalResizing = useTerminalStore((state) => state.isResizing) const isTerminalResizing = useTerminalStore((state) => state.isResizing)
/** /**
@@ -84,7 +88,7 @@ export const Notifications = memo(function Notifications() {
{ {
id: 'clear-notifications', id: 'clear-notifications',
handler: () => { handler: () => {
clearNotifications(workflowId) clearNotifications(activeWorkflowId ?? undefined)
}, },
overrides: { overrides: {
allowInEditable: false, allowInEditable: false,
@@ -93,12 +97,15 @@ export const Notifications = memo(function Notifications() {
]) ])
) )
const preventZoomRef = usePreventZoom()
if (visibleNotifications.length === 0) { if (visibleNotifications.length === 0) {
return null return null
} }
return ( return (
<div <div
ref={preventZoomRef}
className={clsx( className={clsx(
'fixed right-[calc(var(--panel-width)+16px)] bottom-[calc(var(--terminal-height)+16px)] z-30 flex flex-col items-end', 'fixed right-[calc(var(--panel-width)+16px)] bottom-[calc(var(--terminal-height)+16px)] z-30 flex flex-col items-end',
!isTerminalResizing && 'transition-[bottom] duration-100 ease-out' !isTerminalResizing && 'transition-[bottom] duration-100 ease-out'
@@ -113,8 +120,8 @@ export const Notifications = memo(function Notifications() {
<div <div
key={notification.id} key={notification.id}
style={{ transform: `translateX(${xOffset}px)` }} style={{ transform: `translateX(${xOffset}px)` }}
className={`relative h-[78px] w-[240px] overflow-hidden rounded-[4px] border bg-[var(--surface-2)] transition-transform duration-200 ${ className={`relative h-[80px] w-[240px] overflow-hidden rounded-[4px] border bg-[var(--surface-2)] transition-transform duration-200 ${
index > 0 ? '-mt-[78px]' : '' index > 0 ? '-mt-[80px]' : ''
}`} }`}
> >
<div className='flex h-full flex-col justify-between px-[8px] pt-[6px] pb-[8px]'> <div className='flex h-full flex-col justify-between px-[8px] pt-[6px] pb-[8px]'>
@@ -137,19 +144,17 @@ export const Notifications = memo(function Notifications() {
{notification.message} {notification.message}
</div> </div>
{hasAction && ( {hasAction && (
<div className='mt-[4px]'> <Button
<Button variant='active'
variant='active' onClick={() => executeAction(notification.id, notification.action!)}
onClick={() => executeAction(notification.id, notification.action!)} className='w-full px-[8px] py-[4px] font-medium text-[12px]'
className='w-full px-[8px] py-[4px] font-medium text-[12px]' >
> {notification.action!.type === 'copilot'
{notification.action!.type === 'copilot' ? 'Fix in Copilot'
? 'Fix in Copilot' : notification.action!.type === 'refresh'
: notification.action!.type === 'refresh' ? 'Refresh'
? 'Refresh' : 'Take action'}
: 'Take action'} </Button>
</Button>
</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -137,29 +137,29 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
() => ({ () => ({
// Paragraph // Paragraph
p: ({ children }: React.HTMLAttributes<HTMLParagraphElement>) => ( p: ({ children }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p className='mb-1 font-base font-season text-[#1f2124] text-sm leading-[1.25rem] last:mb-0 dark:font-[470] dark:text-[#E8E8E8]'> <p className='mb-1 font-base font-season text-[var(--text-primary)] text-sm leading-[1.25rem] last:mb-0 dark:font-[470]'>
{children} {children}
</p> </p>
), ),
// Headings // Headings
h1: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => ( h1: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h1 className='mt-3 mb-3 font-season font-semibold text-2xl text-[var(--text-primary)] dark:text-[#F0F0F0]'> <h1 className='mt-3 mb-3 font-season font-semibold text-2xl text-[var(--text-primary)]'>
{children} {children}
</h1> </h1>
), ),
h2: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => ( h2: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h2 className='mt-2.5 mb-2.5 font-season font-semibold text-[var(--text-primary)] text-xl dark:text-[#F0F0F0]'> <h2 className='mt-2.5 mb-2.5 font-season font-semibold text-[var(--text-primary)] text-xl'>
{children} {children}
</h2> </h2>
), ),
h3: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => ( h3: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h3 className='mt-2 mb-2 font-season font-semibold text-[var(--text-primary)] text-lg dark:text-[#F0F0F0]'> <h3 className='mt-2 mb-2 font-season font-semibold text-[var(--text-primary)] text-lg'>
{children} {children}
</h3> </h3>
), ),
h4: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => ( h4: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h4 className='mt-5 mb-2 font-season font-semibold text-[var(--text-primary)] text-base dark:text-[#F0F0F0]'> <h4 className='mt-5 mb-2 font-season font-semibold text-[var(--text-primary)] text-base'>
{children} {children}
</h4> </h4>
), ),
@@ -167,7 +167,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
// Lists // Lists
ul: ({ children }: React.HTMLAttributes<HTMLUListElement>) => ( ul: ({ children }: React.HTMLAttributes<HTMLUListElement>) => (
<ul <ul
className='mt-1 mb-1 space-y-1 pl-6 font-base font-season text-[#1f2124] dark:font-[470] dark:text-[#E8E8E8]' className='mt-1 mb-1 space-y-1 pl-6 font-base font-season text-[var(--text-primary)] dark:font-[470]'
style={{ listStyleType: 'disc' }} style={{ listStyleType: 'disc' }}
> >
{children} {children}
@@ -175,7 +175,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
), ),
ol: ({ children }: React.HTMLAttributes<HTMLOListElement>) => ( ol: ({ children }: React.HTMLAttributes<HTMLOListElement>) => (
<ol <ol
className='mt-1 mb-1 space-y-1 pl-6 font-base font-season text-[#1f2124] dark:font-[470] dark:text-[#E8E8E8]' className='mt-1 mb-1 space-y-1 pl-6 font-base font-season text-[var(--text-primary)] dark:font-[470]'
style={{ listStyleType: 'decimal' }} style={{ listStyleType: 'decimal' }}
> >
{children} {children}
@@ -186,7 +186,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
ordered, ordered,
}: React.LiHTMLAttributes<HTMLLIElement> & { ordered?: boolean }) => ( }: React.LiHTMLAttributes<HTMLLIElement> & { ordered?: boolean }) => (
<li <li
className='font-base font-season text-[#1f2124] dark:font-[470] dark:text-[#E8E8E8]' className='font-base font-season text-[var(--text-primary)] dark:font-[470]'
style={{ display: 'list-item' }} style={{ display: 'list-item' }}
> >
{children} {children}
@@ -256,14 +256,14 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
: 'javascript' : 'javascript'
return ( return (
<div className='my-6 w-0 min-w-full overflow-hidden rounded-md border border-[var(--border-strong)] bg-[#1F1F1F] text-sm'> <div className='my-6 w-0 min-w-full overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)] text-sm'>
<div className='flex items-center justify-between border-[var(--border-strong)] border-b px-4 py-1.5'> <div className='flex items-center justify-between border-[var(--border-1)] border-b px-4 py-1.5'>
<span className='font-season text-[#A3A3A3] text-xs'> <span className='font-season text-[var(--text-muted)] text-xs'>
{language === 'code' ? viewerLanguage : language} {language === 'code' ? viewerLanguage : language}
</span> </span>
<button <button
onClick={handleCopy} onClick={handleCopy}
className='text-[#A3A3A3] transition-colors hover:text-gray-300' className='text-[var(--text-muted)] transition-colors hover:text-[var(--text-tertiary)]'
title='Copy' title='Copy'
> >
{showCopySuccess ? ( {showCopySuccess ? (
@@ -293,7 +293,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
if (inline) { if (inline) {
return ( return (
<code <code
className='whitespace-normal break-all rounded border border-[var(--border-strong)] bg-[#1F1F1F] px-1 py-0.5 font-mono text-[#eeeeee] text-[0.9em]' className='whitespace-normal break-all rounded border border-[var(--border-1)] bg-[var(--surface-1)] px-1 py-0.5 font-mono text-[0.9em] text-[var(--text-primary)]'
{...props} {...props}
> >
{children} {children}
@@ -309,35 +309,33 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
// Bold text // Bold text
strong: ({ children }: React.HTMLAttributes<HTMLElement>) => ( strong: ({ children }: React.HTMLAttributes<HTMLElement>) => (
<strong className='font-semibold text-[var(--text-primary)] dark:text-[#F0F0F0]'> <strong className='font-semibold text-[var(--text-primary)]'>{children}</strong>
{children}
</strong>
), ),
// Bold text (alternative) // Bold text (alternative)
b: ({ children }: React.HTMLAttributes<HTMLElement>) => ( b: ({ children }: React.HTMLAttributes<HTMLElement>) => (
<b className='font-semibold text-[var(--text-primary)] dark:text-[#F0F0F0]'>{children}</b> <b className='font-semibold text-[var(--text-primary)]'>{children}</b>
), ),
// Italic text // Italic text
em: ({ children }: React.HTMLAttributes<HTMLElement>) => ( em: ({ children }: React.HTMLAttributes<HTMLElement>) => (
<em className='text-[#1f2124] italic dark:text-[#E8E8E8]'>{children}</em> <em className='text-[var(--text-primary)] italic'>{children}</em>
), ),
// Italic text (alternative) // Italic text (alternative)
i: ({ children }: React.HTMLAttributes<HTMLElement>) => ( i: ({ children }: React.HTMLAttributes<HTMLElement>) => (
<i className='text-[#1f2124] italic dark:text-[#E8E8E8]'>{children}</i> <i className='text-[var(--text-primary)] italic'>{children}</i>
), ),
// Blockquotes // Blockquotes
blockquote: ({ children }: React.HTMLAttributes<HTMLQuoteElement>) => ( blockquote: ({ children }: React.HTMLAttributes<HTMLQuoteElement>) => (
<blockquote className='my-4 border-[var(--border-strong)] border-l-4 py-1 pl-4 font-season text-[#3a3d41] italic dark:border-gray-600 dark:text-[#E0E0E0]'> <blockquote className='my-4 border-[var(--border-1)] border-l-4 py-1 pl-4 font-season text-[var(--text-secondary)] italic'>
{children} {children}
</blockquote> </blockquote>
), ),
// Horizontal rule // Horizontal rule
hr: () => <hr className='my-8 border-[var(--divider)] border-t dark:border-gray-400/[.07]' />, hr: () => <hr className='my-8 border-[var(--divider)] border-t' />,
// Links // Links
a: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => ( a: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
@@ -349,29 +347,31 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
// Tables // Tables
table: ({ children }: React.TableHTMLAttributes<HTMLTableElement>) => ( table: ({ children }: React.TableHTMLAttributes<HTMLTableElement>) => (
<div className='my-4 max-w-full overflow-x-auto'> <div className='my-4 max-w-full overflow-x-auto'>
<table className='min-w-full table-auto border border-[var(--border)] font-season text-sm dark:border-gray-600'> <table className='min-w-full table-auto border border-[var(--border-1)] font-season text-sm'>
{children} {children}
</table> </table>
</div> </div>
), ),
thead: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => ( thead: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<thead className='bg-[var(--surface-9)] text-left dark:bg-[#2A2A2A]'>{children}</thead> <thead className='bg-[var(--surface-5)] text-left dark:bg-[var(--surface-4)]'>
{children}
</thead>
), ),
tbody: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => ( tbody: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<tbody className='divide-y divide-[var(--border)] dark:divide-gray-600'>{children}</tbody> <tbody className='divide-y divide-[var(--border-1)]'>{children}</tbody>
), ),
tr: ({ children }: React.HTMLAttributes<HTMLTableRowElement>) => ( tr: ({ children }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr className='border-[var(--border)] border-b transition-colors hover:bg-[var(--surface-9)] dark:border-gray-600 dark:hover:bg-[#2A2A2A]/60'> <tr className='border-[var(--border-1)] border-b transition-colors hover:bg-[var(--surface-5)] dark:hover:bg-[var(--surface-4)]/60'>
{children} {children}
</tr> </tr>
), ),
th: ({ children }: React.ThHTMLAttributes<HTMLTableCellElement>) => ( th: ({ children }: React.ThHTMLAttributes<HTMLTableCellElement>) => (
<th className='border-[var(--border)] border-r px-4 py-2 align-top font-base text-[#3a3d41] last:border-r-0 dark:border-gray-600 dark:font-[470] dark:text-[#E0E0E0]'> <th className='border-[var(--border-1)] border-r px-4 py-2 align-top font-base text-[var(--text-secondary)] last:border-r-0 dark:font-[470]'>
{children} {children}
</th> </th>
), ),
td: ({ children }: React.TdHTMLAttributes<HTMLTableCellElement>) => ( td: ({ children }: React.TdHTMLAttributes<HTMLTableCellElement>) => (
<td className='break-words border-[var(--border)] border-r px-4 py-2 align-top font-base text-[#1f2124] last:border-r-0 dark:border-gray-600 dark:font-[470] dark:text-[#E8E8E8]'> <td className='break-words border-[var(--border-1)] border-r px-4 py-2 align-top font-base text-[var(--text-primary)] last:border-r-0 dark:font-[470]'>
{children} {children}
</td> </td>
), ),
@@ -390,7 +390,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
) )
return ( return (
<div className='copilot-markdown-wrapper max-w-full space-y-3 break-words font-base font-season text-[#1f2124] text-sm leading-[1.25rem] dark:font-[470] dark:text-[#E8E8E8]'> <div className='copilot-markdown-wrapper max-w-full space-y-3 break-words font-base font-season text-[var(--text-primary)] text-sm leading-[1.25rem] dark:font-[470]'>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}> <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{content} {content}
</ReactMarkdown> </ReactMarkdown>

View File

@@ -36,8 +36,8 @@ interface ShimmerOverlayTextProps {
function ShimmerOverlayText({ label, value, active = false }: ShimmerOverlayTextProps) { function ShimmerOverlayText({ label, value, active = false }: ShimmerOverlayTextProps) {
return ( return (
<span className='relative inline-block'> <span className='relative inline-block'>
<span style={{ color: '#B8B8B8' }}>{label}</span> <span className='text-[var(--text-tertiary)]'>{label}</span>
<span style={{ color: 'var(--text-muted)' }}>{value}</span> <span className='text-[var(--text-muted)]'>{value}</span>
{active ? ( {active ? (
<span <span
aria-hidden='true' aria-hidden='true'
@@ -194,14 +194,11 @@ export function ThinkingBlock({
</button> </button>
{isExpanded && ( {isExpanded && (
<div className='ml-1 border-[var(--border-strong)] border-l-2 pl-2'> <div className='ml-1 border-[var(--border-1)] border-l-2 pl-2'>
<pre <pre className='whitespace-pre-wrap font-[470] font-season text-[12px] text-[var(--text-tertiary)] leading-[1.15rem]'>
className='whitespace-pre-wrap font-[470] font-season text-[12px] leading-[1.15rem]'
style={{ color: '#B8B8B8' }}
>
{content} {content}
{isStreaming && ( {isStreaming && (
<span className='ml-1 inline-block h-2 w-1 animate-pulse bg-[#B8B8B8]' /> <span className='ml-1 inline-block h-2 w-1 animate-pulse bg-[var(--text-tertiary)]' />
)} )}
</pre> </pre>
</div> </div>

View File

@@ -108,6 +108,8 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
const { const {
showRestoreConfirmation, showRestoreConfirmation,
showCheckpointDiscardModal, showCheckpointDiscardModal,
isReverting,
isProcessingDiscard,
pendingEditRef, pendingEditRef,
setShowCheckpointDiscardModal, setShowCheckpointDiscardModal,
handleRevertToCheckpoint, handleRevertToCheckpoint,
@@ -265,30 +267,35 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
{/* Inline Checkpoint Discard Confirmation - shown below input in edit mode */} {/* Inline Checkpoint Discard Confirmation - shown below input in edit mode */}
{showCheckpointDiscardModal && ( {showCheckpointDiscardModal && (
<div className='mt-[8px] rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] p-[10px] dark:bg-[var(--surface-9)]'> <div className='mt-[8px] rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] p-[10px]'>
<p className='mb-[8px] text-[var(--text-primary)] text-sm'> <p className='mb-[8px] text-[12px] text-[var(--text-primary)]'>
Continue from a previous message? Continue from a previous message?
</p> </p>
<div className='flex gap-[6px]'> <div className='flex gap-[8px]'>
<Button <Button
onClick={handleCancelCheckpointDiscard} onClick={handleCancelCheckpointDiscard}
variant='default' variant='active'
className='flex flex-1 items-center justify-center gap-[6px] px-[8px] py-[4px] text-xs' size='sm'
className='flex-1'
disabled={isProcessingDiscard}
> >
<span>Cancel</span> Cancel
<span className='text-[10px] text-[var(--text-muted)]'>(Esc)</span>
</Button> </Button>
<Button <Button
onClick={handleContinueAndRevert} onClick={handleContinueAndRevert}
variant='outline' variant='destructive'
className='flex-1 px-[8px] py-[4px] text-xs' size='sm'
className='flex-1'
disabled={isProcessingDiscard}
> >
Revert {isProcessingDiscard ? 'Reverting...' : 'Revert'}
</Button> </Button>
<Button <Button
onClick={handleContinueWithoutRevert} onClick={handleContinueWithoutRevert}
variant='outline' variant='tertiary'
className='flex-1 px-[8px] py-[4px] text-xs' size='sm'
className='flex-1'
disabled={isProcessingDiscard}
> >
Continue Continue
</Button> </Button>
@@ -312,11 +319,11 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
onClick={handleMessageClick} onClick={handleMessageClick}
onMouseEnter={() => setIsHoveringMessage(true)} onMouseEnter={() => setIsHoveringMessage(true)}
onMouseLeave={() => setIsHoveringMessage(false)} onMouseLeave={() => setIsHoveringMessage(false)}
className='group relative w-full cursor-pointer rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[6px] py-[6px] transition-all duration-200 hover:border-[var(--surface-14)] hover:bg-[var(--surface-9)] dark:bg-[var(--surface-9)] dark:hover:border-[var(--surface-13)] dark:hover:bg-[var(--surface-11)]' className='group relative w-full cursor-pointer rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[6px] py-[6px] transition-all duration-200 hover:border-[var(--surface-7)] hover:bg-[var(--surface-5)] dark:bg-[var(--surface-5)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]'
> >
<div <div
ref={messageContentRef} ref={messageContentRef}
className={`relative whitespace-pre-wrap break-words px-[2px] py-1 font-medium font-sans text-[#0D0D0D] text-sm leading-[1.25rem] dark:text-gray-100 ${isSendingMessage && isLastUserMessage && isHoveringMessage ? 'pr-7' : ''} ${!isExpanded && needsExpansion ? 'max-h-[60px] overflow-hidden' : 'overflow-visible'}`} className={`relative whitespace-pre-wrap break-words px-[2px] py-1 font-medium font-sans text-[var(--text-primary)] text-sm leading-[1.25rem] ${isSendingMessage && isLastUserMessage && isHoveringMessage ? 'pr-7' : ''} ${!isExpanded && needsExpansion ? 'max-h-[60px] overflow-hidden' : 'overflow-visible'}`}
> >
{(() => { {(() => {
const text = message.content || '' const text = message.content || ''
@@ -358,7 +365,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
{/* Gradient fade when truncated - applies to entire message box */} {/* Gradient fade when truncated - applies to entire message box */}
{!isExpanded && needsExpansion && ( {!isExpanded && needsExpansion && (
<div className='pointer-events-none absolute right-0 bottom-0 left-0 h-6 bg-gradient-to-t from-0% from-[var(--surface-6)] via-40% via-[var(--surface-6)]/70 to-100% to-transparent group-hover:from-[var(--surface-9)] group-hover:via-[var(--surface-9)]/70 dark:from-[var(--surface-9)] dark:via-[var(--surface-9)]/70 dark:group-hover:from-[var(--surface-11)] dark:group-hover:via-[var(--surface-11)]/70' /> <div className='pointer-events-none absolute right-0 bottom-0 left-0 h-6 bg-gradient-to-t from-0% from-[var(--surface-5)] via-40% via-[var(--surface-5)]/70 to-100% to-transparent group-hover:from-[var(--surface-5)] group-hover:via-[var(--surface-5)]/70 dark:from-[var(--surface-5)] dark:via-[var(--surface-5)]/70 dark:group-hover:from-[var(--border-1)] dark:group-hover:via-[var(--border-1)]/70' />
)} )}
{/* Abort button when hovering and response is generating (only on last user message) */} {/* Abort button when hovering and response is generating (only on last user message) */}
@@ -369,7 +376,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
e.stopPropagation() e.stopPropagation()
abortMessage() abortMessage()
}} }}
className='h-[20px] w-[20px] rounded-full bg-[#C0C0C0] p-0 transition-colors hover:bg-[#D0D0D0] dark:bg-[#C0C0C0] dark:hover:bg-[#D0D0D0]' className='h-[20px] w-[20px] rounded-full bg-[var(--c-C0C0C0)] p-0 transition-colors hover:bg-[var(--c-D0D0D0)]'
title='Stop generation' title='Stop generation'
> >
<svg <svg
@@ -406,29 +413,30 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
{/* Inline Restore Checkpoint Confirmation */} {/* Inline Restore Checkpoint Confirmation */}
{showRestoreConfirmation && ( {showRestoreConfirmation && (
<div className='mt-[8px] rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] p-[10px] dark:bg-[var(--surface-9)]'> <div className='mt-[8px] rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] p-[10px]'>
<p className='mb-[8px] text-[var(--text-primary)] text-sm'> <p className='mb-[8px] text-[12px] text-[var(--text-primary)]'>
Revert to checkpoint? This will restore your workflow to the state saved at this Revert to checkpoint? This will restore your workflow to the state saved at this
checkpoint.{' '} checkpoint.{' '}
<span className='font-medium text-[var(--text-error)]'> <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
This action cannot be undone.
</span>
</p> </p>
<div className='flex gap-[6px]'> <div className='flex gap-[8px]'>
<Button <Button
onClick={handleCancelRevert} onClick={handleCancelRevert}
variant='default' variant='active'
className='flex flex-1 items-center justify-center gap-[6px] px-[8px] py-[4px] text-xs' size='sm'
className='flex-1'
disabled={isReverting}
> >
<span>Cancel</span> Cancel
<span className='text-[10px] text-[var(--text-muted)]'>(Esc)</span>
</Button> </Button>
<Button <Button
onClick={handleConfirmRevert} onClick={handleConfirmRevert}
variant='outline' variant='destructive'
className='flex-1 px-[8px] py-[4px] text-xs' size='sm'
className='flex-1'
disabled={isReverting}
> >
Revert {isReverting ? 'Reverting...' : 'Revert'}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -26,6 +26,8 @@ export function useCheckpointManagement(
) { ) {
const [showRestoreConfirmation, setShowRestoreConfirmation] = useState(false) const [showRestoreConfirmation, setShowRestoreConfirmation] = useState(false)
const [showCheckpointDiscardModal, setShowCheckpointDiscardModal] = useState(false) const [showCheckpointDiscardModal, setShowCheckpointDiscardModal] = useState(false)
const [isReverting, setIsReverting] = useState(false)
const [isProcessingDiscard, setIsProcessingDiscard] = useState(false)
const pendingEditRef = useRef<{ const pendingEditRef = useRef<{
message: string message: string
fileAttachments?: any[] fileAttachments?: any[]
@@ -48,6 +50,7 @@ export function useCheckpointManagement(
const handleConfirmRevert = useCallback(async () => { const handleConfirmRevert = useCallback(async () => {
if (messageCheckpoints.length > 0) { if (messageCheckpoints.length > 0) {
const latestCheckpoint = messageCheckpoints[0] const latestCheckpoint = messageCheckpoints[0]
setIsReverting(true)
try { try {
await revertToCheckpoint(latestCheckpoint.id) await revertToCheckpoint(latestCheckpoint.id)
@@ -100,6 +103,8 @@ export function useCheckpointManagement(
logger.error('Failed to revert to checkpoint:', error) logger.error('Failed to revert to checkpoint:', error)
setShowRestoreConfirmation(false) setShowRestoreConfirmation(false)
onRevertModeChange?.(false) onRevertModeChange?.(false)
} finally {
setIsReverting(false)
} }
} }
}, [ }, [
@@ -125,50 +130,55 @@ export function useCheckpointManagement(
* Reverts to checkpoint then proceeds with pending edit * Reverts to checkpoint then proceeds with pending edit
*/ */
const handleContinueAndRevert = useCallback(async () => { const handleContinueAndRevert = useCallback(async () => {
if (messageCheckpoints.length > 0) { setIsProcessingDiscard(true)
const latestCheckpoint = messageCheckpoints[0] try {
try { if (messageCheckpoints.length > 0) {
await revertToCheckpoint(latestCheckpoint.id) const latestCheckpoint = messageCheckpoints[0]
try {
await revertToCheckpoint(latestCheckpoint.id)
const { messageCheckpoints: currentCheckpoints } = useCopilotStore.getState() const { messageCheckpoints: currentCheckpoints } = useCopilotStore.getState()
const updatedCheckpoints = { const updatedCheckpoints = {
...currentCheckpoints, ...currentCheckpoints,
[message.id]: messageCheckpoints.slice(1), [message.id]: messageCheckpoints.slice(1),
}
useCopilotStore.setState({ messageCheckpoints: updatedCheckpoints })
logger.info('Reverted to checkpoint before editing message', {
messageId: message.id,
checkpointId: latestCheckpoint.id,
})
} catch (error) {
logger.error('Failed to revert to checkpoint:', error)
} }
useCopilotStore.setState({ messageCheckpoints: updatedCheckpoints })
logger.info('Reverted to checkpoint before editing message', {
messageId: message.id,
checkpointId: latestCheckpoint.id,
})
} catch (error) {
logger.error('Failed to revert to checkpoint:', error)
} }
}
setShowCheckpointDiscardModal(false) setShowCheckpointDiscardModal(false)
const { sendMessage } = useCopilotStore.getState() const { sendMessage } = useCopilotStore.getState()
if (pendingEditRef.current) { if (pendingEditRef.current) {
const { message: msg, fileAttachments, contexts } = pendingEditRef.current const { message: msg, fileAttachments, contexts } = pendingEditRef.current
const editIndex = messages.findIndex((m) => m.id === message.id) const editIndex = messages.findIndex((m) => m.id === message.id)
if (editIndex !== -1) { if (editIndex !== -1) {
const truncatedMessages = messages.slice(0, editIndex) const truncatedMessages = messages.slice(0, editIndex)
const updatedMessage = { const updatedMessage = {
...message, ...message,
content: msg, content: msg,
fileAttachments: fileAttachments || message.fileAttachments, fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts, contexts: contexts || (message as any).contexts,
}
useCopilotStore.setState({ messages: [...truncatedMessages, updatedMessage] })
await sendMessage(msg, {
fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts,
messageId: message.id,
})
} }
useCopilotStore.setState({ messages: [...truncatedMessages, updatedMessage] }) pendingEditRef.current = null
await sendMessage(msg, {
fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts,
messageId: message.id,
})
} }
pendingEditRef.current = null } finally {
setIsProcessingDiscard(false)
} }
}, [messageCheckpoints, revertToCheckpoint, message, messages]) }, [messageCheckpoints, revertToCheckpoint, message, messages])
@@ -252,6 +262,8 @@ export function useCheckpointManagement(
// State // State
showRestoreConfirmation, showRestoreConfirmation,
showCheckpointDiscardModal, showCheckpointDiscardModal,
isReverting,
isProcessingDiscard,
pendingEditRef, pendingEditRef,
// Operations // Operations

View File

@@ -35,9 +35,9 @@ import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId
/** /**
* Shared border and background styles * Shared border and background styles
*/ */
const SURFACE_5 = 'bg-[var(--surface-5)]' const SURFACE_5 = 'bg-[var(--surface-4)]'
const SURFACE_9 = 'bg-[var(--surface-9)]' const SURFACE_9 = 'bg-[var(--surface-5)]'
const BORDER_STRONG = 'border-[var(--border-strong)]' const BORDER_STRONG = 'border-[var(--border-1)]'
export interface PlanModeSectionProps { export interface PlanModeSectionProps {
/** /**
@@ -184,7 +184,7 @@ const PlanModeSection: React.FC<PlanModeSectionProps> = ({
style={{ height: `${height}px` }} style={{ height: `${height}px` }}
> >
{/* Header with build/edit/save/clear buttons */} {/* Header with build/edit/save/clear buttons */}
<div className='flex flex-shrink-0 items-center justify-between border-[var(--border-strong)] border-b py-[6px] pr-[2px] pl-[12px]'> <div className='flex flex-shrink-0 items-center justify-between border-[var(--border-1)] border-b py-[6px] pr-[2px] pl-[12px]'>
<span className='font-[500] text-[11px] text-[var(--text-secondary)] uppercase tracking-wide'> <span className='font-[500] text-[11px] text-[var(--text-secondary)] uppercase tracking-wide'>
Workflow Plan Workflow Plan
</span> </span>
@@ -265,7 +265,7 @@ const PlanModeSection: React.FC<PlanModeSectionProps> = ({
className={cn( className={cn(
'group flex h-[20px] w-full cursor-ns-resize items-center justify-center border-t', 'group flex h-[20px] w-full cursor-ns-resize items-center justify-center border-t',
BORDER_STRONG, BORDER_STRONG,
'transition-colors hover:bg-[var(--surface-9)]', 'transition-colors hover:bg-[var(--surface-5)]',
isResizing && SURFACE_9 isResizing && SURFACE_9
)} )}
onMouseDown={handleResizeStart} onMouseDown={handleResizeStart}

View File

@@ -66,7 +66,7 @@ export const TodoList = memo(function TodoList({
return ( return (
<div <div
className={cn( className={cn(
'w-full rounded-t-[4px] rounded-b-none border-[var(--surface-11)] border-x border-t bg-[var(--surface-6)] dark:bg-[var(--surface-9)]', 'w-full rounded-t-[4px] rounded-b-none border-[var(--border-1)] border-x border-t bg-[var(--surface-5)] dark:bg-[var(--surface-5)]',
className className
)} )}
> >
@@ -92,7 +92,7 @@ export const TodoList = memo(function TodoList({
<div className='flex flex-1 items-center gap-[8px] pl-[10px]'> <div className='flex flex-1 items-center gap-[8px] pl-[10px]'>
{/* Progress bar */} {/* Progress bar */}
<div className='h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--surface-11)]'> <div className='h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--border-1)]'>
<div <div
className='h-full bg-[var(--brand-400)] transition-all duration-300 ease-out' className='h-full bg-[var(--brand-400)] transition-all duration-300 ease-out'
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
@@ -119,8 +119,8 @@ export const TodoList = memo(function TodoList({
<div <div
key={todo.id} key={todo.id}
className={cn( className={cn(
'flex items-start gap-2 px-3 py-1.5 transition-colors hover:bg-[var(--surface-9)]/50 dark:hover:bg-[var(--surface-11)]/50', 'flex items-start gap-2 px-3 py-1.5 transition-colors hover:bg-[var(--surface-5)]/50 dark:hover:bg-[var(--border-1)]/50',
index !== todos.length - 1 && 'border-[var(--surface-11)] border-b' index !== todos.length - 1 && 'border-[var(--border-1)] border-b'
)} )}
> >
{todo.executing ? ( {todo.executing ? (
@@ -133,7 +133,7 @@ export const TodoList = memo(function TodoList({
'mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border transition-all', 'mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border transition-all',
todo.completed todo.completed
? 'border-[var(--brand-400)] bg-[var(--brand-400)]' ? 'border-[var(--brand-400)] bg-[var(--brand-400)]'
: 'border-[#707070]' : 'border-[var(--text-muted)]'
)} )}
> >
{todo.completed ? <Check className='h-3 w-3 text-white' strokeWidth={3} /> : null} {todo.completed ? <Check className='h-3 w-3 text-white' strokeWidth={3} /> : null}
@@ -143,7 +143,9 @@ export const TodoList = memo(function TodoList({
<span <span
className={cn( className={cn(
'flex-1 font-base text-[12px] leading-relaxed', 'flex-1 font-base text-[12px] leading-relaxed',
todo.completed ? 'text-[var(--text-muted)] line-through' : 'text-[var(--white)]' todo.completed
? 'text-[var(--text-muted)] line-through'
: 'text-[var(--text-primary)]'
)} )}
> >
{todo.content} {todo.content}

View File

@@ -1,7 +1,6 @@
'use client' 'use client'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { Button, Code } from '@/components/emcn' import { Button, Code } from '@/components/emcn'
import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool'
import { getClientTool } from '@/lib/copilot/tools/client/manager' import { getClientTool } from '@/lib/copilot/tools/client/manager'
@@ -141,13 +140,11 @@ function ShimmerOverlayText({
}: ShimmerOverlayTextProps) { }: ShimmerOverlayTextProps) {
const [actionVerb, remainder] = splitActionVerb(text) const [actionVerb, remainder] = splitActionVerb(text)
// Special tools: use gradient for entire text // Special tools: use tertiary-2 color for entire text with shimmer
if (isSpecial) { if (isSpecial) {
return ( return (
<span className={`relative inline-block ${className || ''}`}> <span className={`relative inline-block ${className || ''}`}>
<span className='bg-gradient-to-r from-[#7c3aed] to-[#9061f9] bg-clip-text text-transparent dark:from-[#B99FFD] dark:to-[#D1BFFF]'> <span className='text-[var(--brand-tertiary-2)]'>{text}</span>
{text}
</span>
{active ? ( {active ? (
<span <span
aria-hidden='true' aria-hidden='true'
@@ -157,7 +154,7 @@ function ShimmerOverlayText({
className='block text-transparent' className='block text-transparent'
style={{ style={{
backgroundImage: backgroundImage:
'linear-gradient(90deg, rgba(142,76,251,0) 0%, rgba(255,255,255,0.6) 50%, rgba(142,76,251,0) 100%)', 'linear-gradient(90deg, rgba(51,196,129,0) 0%, rgba(255,255,255,0.6) 50%, rgba(51,196,129,0) 100%)',
backgroundSize: '200% 100%', backgroundSize: '200% 100%',
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
@@ -186,8 +183,12 @@ function ShimmerOverlayText({
<span className={`relative inline-block ${className || ''}`}> <span className={`relative inline-block ${className || ''}`}>
{actionVerb ? ( {actionVerb ? (
<> <>
<span className='text-[#1f2124] dark:text-[#B8B8B8]'>{actionVerb}</span> <span className='text-[var(--text-primary)] dark:text-[var(--text-tertiary)]'>
<span className='text-[#6b7075] dark:text-[var(--text-muted)]'>{remainder}</span> {actionVerb}
</span>
<span className='text-[var(--text-secondary)] dark:text-[var(--text-muted)]'>
{remainder}
</span>
</> </>
) : ( ) : (
<span>{text}</span> <span>{text}</span>
@@ -504,13 +505,11 @@ function RunSkipButtons({
// Standardized buttons for all interrupt tools: Allow, Always Allow, Skip // Standardized buttons for all interrupt tools: Allow, Always Allow, Skip
return ( return (
<div className='mt-[12px] flex gap-[6px]'> <div className='mt-[12px] flex gap-[6px]'>
<Button onClick={onRun} disabled={isProcessing} variant='primary'> <Button onClick={onRun} disabled={isProcessing} variant='tertiary'>
{isProcessing ? <Loader2 className='mr-1 h-3 w-3 animate-spin' /> : null} {isProcessing ? 'Allowing...' : 'Allow'}
Allow
</Button> </Button>
<Button onClick={onAlwaysAllow} disabled={isProcessing} variant='default'> <Button onClick={onAlwaysAllow} disabled={isProcessing} variant='default'>
{isProcessing ? <Loader2 className='mr-1 h-3 w-3 animate-spin' /> : null} {isProcessing ? 'Allowing...' : 'Always Allow'}
Always Allow
</Button> </Button>
<Button onClick={onSkip} disabled={isProcessing} variant='default'> <Button onClick={onSkip} disabled={isProcessing} variant='default'>
Skip Skip
@@ -606,11 +605,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
const url = editedParams.url || '' const url = editedParams.url || ''
const method = (editedParams.method || '').toUpperCase() const method = (editedParams.method || '').toUpperCase()
return ( return (
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]'> <div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
<table className='w-full table-fixed bg-transparent'> <table className='w-full table-fixed bg-transparent'>
<thead className='bg-transparent'> <thead className='bg-transparent'>
<tr className='border-[var(--border-strong)] border-b bg-transparent'> <tr className='border-[var(--border-1)] border-b bg-transparent'>
<th className='w-[26%] border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'> <th className='w-[26%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Method Method
</th> </th>
<th className='w-[74%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'> <th className='w-[74%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
@@ -619,8 +618,8 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
</tr> </tr>
</thead> </thead>
<tbody className='bg-transparent'> <tbody className='bg-transparent'>
<tr className='group relative border-[var(--border-strong)] border-t bg-transparent'> <tr className='group relative border-[var(--border-1)] border-t bg-transparent'>
<td className='relative w-[26%] border-[var(--border-strong)] border-r bg-transparent p-0'> <td className='relative w-[26%] border-[var(--border-1)] border-r bg-transparent p-0'>
<div className='px-[10px] py-[8px]'> <div className='px-[10px] py-[8px]'>
<input <input
type='text' type='text'
@@ -668,11 +667,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
}) })
return ( return (
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]'> <div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
<table className='w-full table-fixed bg-transparent'> <table className='w-full table-fixed bg-transparent'>
<thead className='bg-transparent'> <thead className='bg-transparent'>
<tr className='border-[var(--border-strong)] border-b bg-transparent'> <tr className='border-[var(--border-1)] border-b bg-transparent'>
<th className='w-[36%] border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'> <th className='w-[36%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Name Name
</th> </th>
<th className='w-[64%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'> <th className='w-[64%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
@@ -682,8 +681,8 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
</thead> </thead>
<tbody className='bg-transparent'> <tbody className='bg-transparent'>
{normalizedEntries.length === 0 ? ( {normalizedEntries.length === 0 ? (
<tr className='border-[var(--border-strong)] border-t bg-transparent'> <tr className='border-[var(--border-1)] border-t bg-transparent'>
<td colSpan={2} className='px-[10px] py-[8px] text-muted-foreground text-xs'> <td colSpan={2} className='px-[10px] py-[8px] text-[var(--text-muted)] text-xs'>
No variables provided No variables provided
</td> </td>
</tr> </tr>
@@ -691,9 +690,9 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
normalizedEntries.map(([originalKey, name, value]) => ( normalizedEntries.map(([originalKey, name, value]) => (
<tr <tr
key={originalKey} key={originalKey}
className='group relative border-[var(--border-strong)] border-t bg-transparent' className='group relative border-[var(--border-1)] border-t bg-transparent'
> >
<td className='relative w-[36%] border-[var(--border-strong)] border-r bg-transparent p-0'> <td className='relative w-[36%] border-[var(--border-1)] border-r bg-transparent p-0'>
<div className='px-[10px] py-[8px]'> <div className='px-[10px] py-[8px]'>
<input <input
type='text' type='text'
@@ -720,7 +719,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
} }
setEditedParams({ ...editedParams, variables: newVariables }) setEditedParams({ ...editedParams, variables: newVariables })
}} }}
className='w-full bg-transparent font-medium text-foreground text-xs outline-none' className='w-full bg-transparent font-medium text-[var(--text-primary)] text-xs outline-none'
/> />
</div> </div>
</td> </td>
@@ -755,7 +754,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
} }
setEditedParams({ ...editedParams, variables: newVariables }) setEditedParams({ ...editedParams, variables: newVariables })
}} }}
className='w-full bg-transparent font-mono text-muted-foreground text-xs outline-none focus:text-foreground' className='w-full bg-transparent font-mono text-[var(--text-muted)] text-xs outline-none focus:text-[var(--text-primary)]'
/> />
</div> </div>
</td> </td>
@@ -771,24 +770,24 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
if (toolCall.name === 'set_global_workflow_variables') { if (toolCall.name === 'set_global_workflow_variables') {
const ops = Array.isArray(editedParams.operations) ? (editedParams.operations as any[]) : [] const ops = Array.isArray(editedParams.operations) ? (editedParams.operations as any[]) : []
return ( return (
<div className='w-full overflow-hidden rounded border border-muted bg-card'> <div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
<div className='grid grid-cols-3 gap-0 border-muted/60 border-b bg-muted/40 py-1.5'> <div className='grid grid-cols-3 gap-0 border-[var(--border-1)] border-b bg-[var(--surface-4)] py-1.5'>
<div className='self-start px-2 font-medium font-season text-[#3a3d41] text-[10px] uppercase tracking-wide dark:text-[#E0E0E0]'> <div className='self-start px-2 font-medium font-season text-[10px] text-[var(--text-secondary)] uppercase tracking-wide'>
Name Name
</div> </div>
<div className='self-start px-2 font-medium font-season text-[#3a3d41] text-[10px] uppercase tracking-wide dark:text-[#E0E0E0]'> <div className='self-start px-2 font-medium font-season text-[10px] text-[var(--text-secondary)] uppercase tracking-wide'>
Type Type
</div> </div>
<div className='self-start px-2 font-medium font-season text-[#3a3d41] text-[10px] uppercase tracking-wide dark:text-[#E0E0E0]'> <div className='self-start px-2 font-medium font-season text-[10px] text-[var(--text-secondary)] uppercase tracking-wide'>
Value Value
</div> </div>
</div> </div>
{ops.length === 0 ? ( {ops.length === 0 ? (
<div className='px-2 py-2 font-[470] font-season text-[#1f2124] text-xs dark:text-[#E8E8E8]'> <div className='px-2 py-2 font-[470] font-season text-[var(--text-primary)] text-xs'>
No operations provided No operations provided
</div> </div>
) : ( ) : (
<div className='divide-y divide-amber-200 dark:divide-amber-800'> <div className='divide-y divide-[var(--border-1)]'>
{ops.map((op, idx) => ( {ops.map((op, idx) => (
<div key={idx} className='grid grid-cols-3 gap-0 py-1.5'> <div key={idx} className='grid grid-cols-3 gap-0 py-1.5'>
<div className='min-w-0 self-start px-2'> <div className='min-w-0 self-start px-2'>
@@ -800,11 +799,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
newOps[idx] = { ...op, name: e.target.value } newOps[idx] = { ...op, name: e.target.value }
setEditedParams({ ...editedParams, operations: newOps }) setEditedParams({ ...editedParams, operations: newOps })
}} }}
className='w-full bg-transparent font-season text-amber-800 text-xs outline-none dark:text-amber-200' className='w-full bg-transparent font-season text-[var(--text-primary)] text-xs outline-none'
/> />
</div> </div>
<div className='self-start px-2'> <div className='self-start px-2'>
<span className='rounded border px-1 py-0.5 font-[470] font-season text-[#1f2124] text-[10px] dark:text-[#E8E8E8]'> <span className='rounded border border-[var(--border-1)] px-1 py-0.5 font-[470] font-season text-[10px] text-[var(--text-primary)]'>
{String(op.type || '')} {String(op.type || '')}
</span> </span>
</div> </div>
@@ -818,10 +817,10 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
newOps[idx] = { ...op, value: e.target.value } newOps[idx] = { ...op, value: e.target.value }
setEditedParams({ ...editedParams, operations: newOps }) setEditedParams({ ...editedParams, operations: newOps })
}} }}
className='w-full bg-transparent font-[470] font-mono text-amber-700 text-xs outline-none focus:text-amber-800 dark:text-amber-300 dark:focus:text-amber-200' className='w-full bg-transparent font-[470] font-mono text-[var(--text-muted)] text-xs outline-none focus:text-[var(--text-primary)]'
/> />
) : ( ) : (
<span className='font-[470] font-season text-[#1f2124] text-xs dark:text-[#E8E8E8]'> <span className='font-[470] font-season text-[var(--text-primary)] text-xs'>
</span> </span>
)} )}
@@ -864,11 +863,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
const inputEntries = Object.entries(safeInputs) const inputEntries = Object.entries(safeInputs)
return ( return (
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]'> <div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
<table className='w-full table-fixed bg-transparent'> <table className='w-full table-fixed bg-transparent'>
<thead className='bg-transparent'> <thead className='bg-transparent'>
<tr className='border-[var(--border-strong)] border-b bg-transparent'> <tr className='border-[var(--border-1)] border-b bg-transparent'>
<th className='w-[36%] border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'> <th className='w-[36%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Input Input
</th> </th>
<th className='w-[64%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'> <th className='w-[64%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
@@ -878,8 +877,8 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
</thead> </thead>
<tbody className='bg-transparent'> <tbody className='bg-transparent'>
{inputEntries.length === 0 ? ( {inputEntries.length === 0 ? (
<tr className='border-[var(--border-strong)] border-t bg-transparent'> <tr className='border-[var(--border-1)] border-t bg-transparent'>
<td colSpan={2} className='px-[10px] py-[8px] text-muted-foreground text-xs'> <td colSpan={2} className='px-[10px] py-[8px] text-[var(--text-muted)] text-xs'>
No inputs provided No inputs provided
</td> </td>
</tr> </tr>
@@ -887,11 +886,13 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
inputEntries.map(([key, value]) => ( inputEntries.map(([key, value]) => (
<tr <tr
key={key} key={key}
className='group relative border-[var(--border-strong)] border-t bg-transparent' className='group relative border-[var(--border-1)] border-t bg-transparent'
> >
<td className='relative w-[36%] border-[var(--border-strong)] border-r bg-transparent p-0'> <td className='relative w-[36%] border-[var(--border-1)] border-r bg-transparent p-0'>
<div className='px-[10px] py-[8px]'> <div className='px-[10px] py-[8px]'>
<span className='truncate font-medium text-foreground text-xs'>{key}</span> <span className='truncate font-medium text-[var(--text-primary)] text-xs'>
{key}
</span>
</div> </div>
</td> </td>
<td className='relative w-[64%] bg-transparent p-0'> <td className='relative w-[64%] bg-transparent p-0'>
@@ -926,7 +927,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
setEditedParams({ ...editedParams, [key]: e.target.value }) setEditedParams({ ...editedParams, [key]: e.target.value })
} }
}} }}
className='w-full bg-transparent font-mono text-muted-foreground text-xs outline-none focus:text-foreground' className='w-full bg-transparent font-mono text-[var(--text-muted)] text-xs outline-none focus:text-[var(--text-primary)]'
/> />
</div> </div>
</td> </td>
@@ -959,7 +960,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
text={displayName} text={displayName}
active={isLoadingState} active={isLoadingState}
isSpecial={isSpecial} isSpecial={isSpecial}
className='font-[470] font-season text-[#3a3d41] text-sm dark:text-[#939393]' className='font-[470] font-season text-[var(--text-secondary)] text-sm dark:text-[var(--text-muted)]'
/> />
</div> </div>
<div className='mt-[8px]'>{renderPendingDetails()}</div> <div className='mt-[8px]'>{renderPendingDetails()}</div>
@@ -1010,7 +1011,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
text={displayName} text={displayName}
active={isLoadingState} active={isLoadingState}
isSpecial={false} isSpecial={false}
className='font-[470] font-season text-[#939393] text-sm dark:text-[#939393]' className='font-[470] font-season text-[var(--text-muted)] text-sm'
/> />
</div> </div>
{code && ( {code && (
@@ -1062,7 +1063,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
text={displayName} text={displayName}
active={isLoadingState} active={isLoadingState}
isSpecial={isSpecial} isSpecial={isSpecial}
className='font-[470] font-season text-[#3a3d41] text-sm dark:text-[#939393]' className='font-[470] font-season text-[var(--text-secondary)] text-sm dark:text-[var(--text-muted)]'
/> />
</div> </div>
{isExpandableTool && expanded && <div>{renderPendingDetails()}</div>} {isExpandableTool && expanded && <div>{renderPendingDetails()}</div>}
@@ -1104,7 +1105,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
onStateChange?.('background') onStateChange?.('background')
} catch {} } catch {}
}} }}
variant='primary' variant='tertiary'
title='Move to Background' title='Move to Background'
> >
Move to Background Move to Background
@@ -1135,7 +1136,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
onStateChange?.('background') onStateChange?.('background')
} catch {} } catch {}
}} }}
variant='primary' variant='tertiary'
title='Wake' title='Wake'
> >
Wake Wake

View File

@@ -71,7 +71,7 @@ export function AttachedFilesDisplay({
{files.map((file) => ( {files.map((file) => (
<div <div
key={file.id} key={file.id}
className='group relative h-16 w-16 flex-shrink-0 cursor-pointer overflow-hidden rounded-md border border-border/50 bg-muted/20 transition-all hover:bg-muted/40' className='group relative h-16 w-16 flex-shrink-0 cursor-pointer overflow-hidden rounded-md border bg-muted/20 transition-all hover:bg-muted/40'
title={`${file.name} (${formatFileSize(file.size)})`} title={`${file.name} (${formatFileSize(file.size)})`}
onClick={() => onFileClick(file)} onClick={() => onFileClick(file)}
> >

View File

@@ -29,9 +29,9 @@ export function ContextUsageIndicator({
const offset = circumference - (percentage / 100) * circumference const offset = circumference - (percentage / 100) * circumference
const color = useMemo(() => { const color = useMemo(() => {
if (percentage >= 90) return '#dc2626' if (percentage >= 90) return 'var(--text-error)'
if (percentage >= 75) return '#d97706' if (percentage >= 75) return 'var(--warning)'
return '#6b7280' return 'var(--text-muted)'
}, [percentage]) }, [percentage])
const displayPercentage = useMemo(() => { const displayPercentage = useMemo(() => {

View File

@@ -29,7 +29,7 @@ function formatTimestamp(iso: string): string {
/** /**
* Common text styling for loading and empty states * Common text styling for loading and empty states
*/ */
const STATE_TEXT_CLASSES = 'px-[8px] py-[8px] text-[#868686] text-[12px] dark:text-[#868686]' const STATE_TEXT_CLASSES = 'px-[8px] py-[8px] text-[12px] text-[var(--text-muted)]'
/** /**
* Loading state component for mention folders * Loading state component for mention folders
@@ -541,9 +541,7 @@ export function MentionMenu({
active={index === submenuActiveIndex} active={index === submenuActiveIndex}
> >
<span className='flex-1 truncate'>{tpl.name}</span> <span className='flex-1 truncate'>{tpl.name}</span>
<span className='text-[#868686] text-[10px] dark:text-[#868686]'> <span className='text-[10px] text-[var(--text-muted)]'>{tpl.stars}</span>
{tpl.stars}
</span>
</PopoverItem> </PopoverItem>
)) ))
)} )}
@@ -745,9 +743,7 @@ export function MentionMenu({
mentionData.templatesList.map((tpl) => ( mentionData.templatesList.map((tpl) => (
<PopoverItem key={tpl.id} onClick={() => insertTemplateMention(tpl)}> <PopoverItem key={tpl.id} onClick={() => insertTemplateMention(tpl)}>
<span className='flex-1 truncate'>{tpl.name}</span> <span className='flex-1 truncate'>{tpl.name}</span>
<span className='text-[#868686] text-[10px] dark:text-[#868686]'> <span className='text-[10px] text-[var(--text-muted)]'>{tpl.stars}</span>
{tpl.stars}
</span>
</PopoverItem> </PopoverItem>
)) ))
)} )}

View File

@@ -619,7 +619,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
<div <div
ref={setInputContainerRef} ref={setInputContainerRef}
className={cn( className={cn(
'relative w-full rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[6px] py-[6px] transition-colors dark:bg-[var(--surface-9)]', 'relative w-full rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[6px] py-[6px] transition-colors dark:bg-[var(--surface-5)]',
fileAttachments.isDragging && 'ring-[1.75px] ring-[var(--brand-secondary)]' fileAttachments.isDragging && 'ring-[1.75px] ring-[var(--brand-secondary)]'
)} )}
onDragEnter={fileAttachments.handleDragEnter} onDragEnter={fileAttachments.handleDragEnter}
@@ -679,7 +679,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
{/* Highlight overlay - must have identical flow as textarea */} {/* Highlight overlay - must have identical flow as textarea */}
<div <div
ref={overlayRef} ref={overlayRef}
className='pointer-events-none absolute top-0 left-0 z-[1] m-0 box-border h-auto max-h-[120px] min-h-[48px] w-full resize-none overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-0 bg-transparent px-[2px] py-1 font-medium font-sans text-[#0D0D0D] text-sm leading-[1.25rem] outline-none [-ms-overflow-style:none] [scrollbar-width:none] [text-rendering:optimizeLegibility] dark:text-gray-100 [&::-webkit-scrollbar]:hidden' className='pointer-events-none absolute top-0 left-0 z-[1] m-0 box-border h-auto max-h-[120px] min-h-[48px] w-full resize-none overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-0 bg-transparent px-[2px] py-1 font-medium font-sans text-[var(--text-primary)] text-sm leading-[1.25rem] outline-none [-ms-overflow-style:none] [scrollbar-width:none] [text-rendering:optimizeLegibility] [&::-webkit-scrollbar]:hidden'
aria-hidden='true' aria-hidden='true'
> >
{renderOverlayContent()} {renderOverlayContent()}
@@ -760,8 +760,8 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
className={cn( className={cn(
'h-[20px] w-[20px] rounded-full p-0 transition-colors', 'h-[20px] w-[20px] rounded-full p-0 transition-colors',
!isAborting !isAborting
? 'bg-[#C0C0C0] hover:bg-[#D0D0D0] dark:bg-[#C0C0C0] dark:hover:bg-[#D0D0D0]' ? 'bg-[var(--c-C0C0C0)] hover:bg-[var(--c-D0D0D0)]'
: 'bg-[#C0C0C0] dark:bg-[#C0C0C0]' : 'bg-[var(--c-C0C0C0)]'
)} )}
title='Stop generation' title='Stop generation'
> >
@@ -787,8 +787,8 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
className={cn( className={cn(
'h-[22px] w-[22px] rounded-full p-0 transition-colors', 'h-[22px] w-[22px] rounded-full p-0 transition-colors',
canSubmit canSubmit
? 'bg-[#C0C0C0] hover:bg-[#D0D0D0] dark:bg-[#C0C0C0] dark:hover:bg-[#D0D0D0]' ? 'bg-[var(--c-C0C0C0)] hover:bg-[var(--c-D0D0D0)]'
: 'bg-[#C0C0C0] dark:bg-[#C0C0C0]' : 'bg-[var(--c-C0C0C0)]'
)} )}
> >
{isLoading ? ( {isLoading ? (

View File

@@ -398,7 +398,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
className='flex h-full flex-col overflow-hidden' className='flex h-full flex-col overflow-hidden'
> >
{/* Header */} {/* Header */}
<div className='flex flex-shrink-0 items-center justify-between gap-[8px] rounded-[4px] bg-[var(--surface-5)] px-[12px] py-[8px]'> <div className='mx-[-1px] flex flex-shrink-0 items-center justify-between gap-[8px] rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] px-[12px] py-[6px]'>
<h2 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'> <h2 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
{currentChat?.title || 'New Chat'} {currentChat?.title || 'New Chat'}
</h2> </h2>
@@ -418,7 +418,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
<ChatHistorySkeleton /> <ChatHistorySkeleton />
</PopoverScrollArea> </PopoverScrollArea>
) : groupedChats.length === 0 ? ( ) : groupedChats.length === 0 ? (
<div className='px-[6px] py-[16px] text-center text-[12px] text-[var(--white)]'> <div className='px-[6px] py-[16px] text-center text-[12px] text-muted-foreground'>
No chats yet No chats yet
</div> </div>
) : ( ) : (

View File

@@ -478,7 +478,7 @@ console.log(limits);`
code={info.endpoint} code={info.endpoint}
language='javascript' language='javascript'
wrapText wrapText
className='!min-h-0 rounded-[4px] border border-[var(--border-strong)]' className='!min-h-0 rounded-[4px] border border-[var(--border-1)]'
/> />
</div> */} </div> */}
@@ -534,7 +534,7 @@ console.log(limits);`
code={getSyncCommand()} code={getSyncCommand()}
language={LANGUAGE_SYNTAX[language]} language={LANGUAGE_SYNTAX[language]}
wrapText wrapText
className='!min-h-0 rounded-[4px] border border-[var(--border-strong)]' className='!min-h-0 rounded-[4px] border border-[var(--border-1)]'
/> />
</div> </div>
@@ -577,7 +577,7 @@ console.log(limits);`
code={getStreamCommand()} code={getStreamCommand()}
language={LANGUAGE_SYNTAX[language]} language={LANGUAGE_SYNTAX[language]}
wrapText wrapText
className='!min-h-0 rounded-[4px] border border-[var(--border-strong)]' className='!min-h-0 rounded-[4px] border border-[var(--border-1)]'
/> />
</div> </div>
@@ -655,7 +655,7 @@ console.log(limits);`
code={getAsyncCommand()} code={getAsyncCommand()}
language={LANGUAGE_SYNTAX[language]} language={LANGUAGE_SYNTAX[language]}
wrapText wrapText
className='!min-h-0 rounded-[4px] border border-[var(--border-strong)]' className='!min-h-0 rounded-[4px] border border-[var(--border-1)]'
/> />
</div> </div>
)} )}

View File

@@ -394,7 +394,7 @@ export function ChatDeploy({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Chat</ModalHeader> <ModalHeader>Delete Chat</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete this chat?{' '} Are you sure you want to delete this chat?{' '}
<span className='text-[var(--text-error)]'> <span className='text-[var(--text-error)]'>
This will remove the chat at "{getEmailDomain()}/chat/{existingChat?.identifier}" This will remove the chat at "{getEmailDomain()}/chat/{existingChat?.identifier}"
@@ -410,13 +410,7 @@ export function ChatDeploy({
> >
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={handleDelete} disabled={isDeleting}>
variant='primary'
onClick={handleDelete}
disabled={isDeleting}
className='gap-[8px] bg-[var(--text-error)] text-[13px] text-white hover:bg-[var(--text-error)]'
>
{isDeleting && <Loader2 className='mr-1 h-4 w-4 animate-spin' />}
{isDeleting ? 'Deleting...' : 'Delete'} {isDeleting ? 'Deleting...' : 'Delete'}
</Button> </Button>
</ModalFooter> </ModalFooter>
@@ -507,11 +501,11 @@ function IdentifierInput({
</Label> </Label>
<div <div
className={cn( className={cn(
'relative flex items-stretch overflow-hidden rounded-[4px] border border-[var(--surface-11)]', 'relative flex items-stretch overflow-hidden rounded-[4px] border border-[var(--border-1)]',
error && 'border-[var(--text-error)]' error && 'border-[var(--text-error)]'
)} )}
> >
<div className='flex items-center whitespace-nowrap bg-[var(--surface-6)] px-[8px] font-medium text-[var(--text-secondary)] text-sm dark:bg-[var(--surface-9)]'> <div className='flex items-center whitespace-nowrap bg-[var(--surface-5)] px-[8px] font-medium text-[var(--text-secondary)] text-sm dark:bg-[var(--surface-5)]'>
{getDomainPrefix()} {getDomainPrefix()}
</div> </div>
<div className='relative flex-1'> <div className='relative flex-1'>
@@ -787,7 +781,7 @@ function AuthSelector({
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'> <Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
{authType === 'email' ? 'Allowed emails' : 'Allowed SSO emails'} {authType === 'email' ? 'Allowed emails' : 'Allowed SSO emails'}
</Label> </Label>
<div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-9)]'> <div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-5)]'>
{invalidEmails.map((email, index) => ( {invalidEmails.map((email, index) => (
<EmailTag <EmailTag
key={`invalid-${index}`} key={`invalid-${index}`}
@@ -851,7 +845,7 @@ function EmailTag({ email, onRemove, disabled, isInvalid }: EmailTagProps) {
'flex w-auto items-center gap-[4px] rounded-[4px] border px-[6px] py-[2px] text-[12px]', 'flex w-auto items-center gap-[4px] rounded-[4px] border px-[6px] py-[2px] text-[12px]',
isInvalid isInvalid
? 'border-[var(--text-error)] bg-[color-mix(in_srgb,var(--text-error)_10%,transparent)] text-[var(--text-error)] dark:bg-[color-mix(in_srgb,var(--text-error)_16%,transparent)]' ? 'border-[var(--text-error)] bg-[color-mix(in_srgb,var(--text-error)_10%,transparent)] text-[var(--text-error)] dark:bg-[color-mix(in_srgb,var(--text-error)_16%,transparent)]'
: 'border-[var(--surface-11)] bg-[var(--surface-5)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]' : 'border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
)} )}
> >
<span className='max-w-[200px] truncate'>{email}</span> <span className='max-w-[200px] truncate'>{email}</span>

View File

@@ -12,7 +12,7 @@ const logger = createLogger('Versions')
/** Shared styling constants aligned with terminal component */ /** Shared styling constants aligned with terminal component */
const HEADER_TEXT_CLASS = 'font-medium text-[var(--text-tertiary)] text-[12px]' const HEADER_TEXT_CLASS = 'font-medium text-[var(--text-tertiary)] text-[12px]'
const ROW_TEXT_CLASS = 'font-medium text-[#D2D2D2] text-[12px]' const ROW_TEXT_CLASS = 'font-medium text-[var(--text-primary)] text-[12px]'
const COLUMN_BASE_CLASS = 'flex-shrink-0' const COLUMN_BASE_CLASS = 'flex-shrink-0'
/** Column width configuration */ /** Column width configuration */
@@ -220,10 +220,10 @@ export function Versions({
<div <div
key={v.id} key={v.id}
className={clsx( className={clsx(
'flex h-[36px] cursor-pointer items-center px-[16px] transition-colors', 'flex h-[36px] cursor-pointer items-center px-[16px] transition-colors duration-100',
isSelected isSelected
? 'bg-[var(--accent)]/10 hover:bg-[var(--accent)]/15' ? 'bg-[var(--accent)]/10 hover:bg-[var(--accent)]/15'
: 'hover:bg-[var(--border)]' : 'hover:bg-[var(--surface-6)] dark:hover:bg-[var(--border)]'
)} )}
onClick={() => handleRowClick(v.version)} onClick={() => handleRowClick(v.version)}
> >

View File

@@ -257,7 +257,7 @@ export function GeneralDeploy({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Load Deployment</ModalHeader> <ModalHeader>Load Deployment</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to load{' '} Are you sure you want to load{' '}
<span className='font-medium text-[var(--text-primary)]'> <span className='font-medium text-[var(--text-primary)]'>
{versionToLoadInfo?.name || `v${versionToLoad}`} {versionToLoadInfo?.name || `v${versionToLoad}`}
@@ -272,11 +272,7 @@ export function GeneralDeploy({
<Button variant='default' onClick={() => setShowLoadDialog(false)}> <Button variant='default' onClick={() => setShowLoadDialog(false)}>
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={confirmLoadDeployment}>
variant='primary'
onClick={confirmLoadDeployment}
className='bg-[var(--text-error)] text-white hover:bg-[var(--text-error)]'
>
Load deployment Load deployment
</Button> </Button>
</ModalFooter> </ModalFooter>
@@ -302,7 +298,7 @@ export function GeneralDeploy({
<Button variant='default' onClick={() => setShowPromoteDialog(false)}> <Button variant='default' onClick={() => setShowPromoteDialog(false)}>
Cancel Cancel
</Button> </Button>
<Button variant='primary' onClick={confirmPromoteToLive}> <Button variant='tertiary' onClick={confirmPromoteToLive}>
Promote to live Promote to live
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -1,8 +1,7 @@
'use client' 'use client'
import React, { useEffect, useRef, useState } from 'react' import { forwardRef, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import { Loader2 } from 'lucide-react'
import { import {
Button, Button,
Combobox, Combobox,
@@ -364,7 +363,7 @@ export function TemplateDeploy({
</p> </p>
<Button <Button
type='button' type='button'
variant='primary' variant='tertiary'
onClick={() => { onClick={() => {
try { try {
const event = new CustomEvent('open-settings', { const event = new CustomEvent('open-settings', {
@@ -425,7 +424,7 @@ export function TemplateDeploy({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Template</ModalHeader> <ModalHeader>Delete Template</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete this template?{' '} Are you sure you want to delete this template?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span> <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p> </p>
@@ -435,19 +434,11 @@ export function TemplateDeploy({
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='destructive'
onClick={handleDelete} onClick={handleDelete}
disabled={deleteMutation.isPending} disabled={deleteMutation.isPending}
className='bg-[var(--text-error)] text-[13px] text-white hover:bg-[var(--text-error)]'
> >
{deleteMutation.isPending ? ( {deleteMutation.isPending ? 'Deleting...' : 'Delete'}
<>
<Loader2 className='mr-1.5 h-3.5 w-3.5 animate-spin' />
Deleting...
</>
) : (
'Delete'
)}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -463,7 +454,7 @@ export function TemplateDeploy({
* Hidden container for OG image capture. * Hidden container for OG image capture.
* Lazy-rendered only when capturing - gets workflow state from store on mount. * Lazy-rendered only when capturing - gets workflow state from store on mount.
*/ */
const OGCaptureContainer = React.forwardRef<HTMLDivElement>((_, ref) => { const OGCaptureContainer = forwardRef<HTMLDivElement>((_, ref) => {
const blocks = useWorkflowStore((state) => state.blocks) const blocks = useWorkflowStore((state) => state.blocks)
const edges = useWorkflowStore((state) => state.edges) const edges = useWorkflowStore((state) => state.edges)
const loops = useWorkflowStore((state) => state.loops) const loops = useWorkflowStore((state) => state.loops)

View File

@@ -2,8 +2,8 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger' import { createLogger } from '@sim/logger'
import clsx from 'clsx'
import { import {
Badge,
Button, Button,
Modal, Modal,
ModalBody, ModalBody,
@@ -628,7 +628,7 @@ export function DeployModal({
{chatExists && ( {chatExists && (
<Button <Button
type='button' type='button'
variant='default' variant='destructive'
onClick={handleChatDelete} onClick={handleChatDelete}
disabled={chatSubmitting} disabled={chatSubmitting}
> >
@@ -637,7 +637,7 @@ export function DeployModal({
)} )}
<Button <Button
type='button' type='button'
variant='primary' variant='tertiary'
onClick={handleChatFormSubmit} onClick={handleChatFormSubmit}
disabled={chatSubmitting || !isChatFormValid} disabled={chatSubmitting || !isChatFormValid}
> >
@@ -667,7 +667,7 @@ export function DeployModal({
{hasExistingTemplate && ( {hasExistingTemplate && (
<Button <Button
type='button' type='button'
variant='default' variant='destructive'
onClick={handleTemplateDelete} onClick={handleTemplateDelete}
disabled={templateSubmitting} disabled={templateSubmitting}
> >
@@ -676,7 +676,7 @@ export function DeployModal({
)} )}
<Button <Button
type='button' type='button'
variant='primary' variant='tertiary'
onClick={handleTemplateFormSubmit} onClick={handleTemplateFormSubmit}
disabled={templateSubmitting || !templateFormValid} disabled={templateSubmitting || !templateFormValid}
> >
@@ -698,7 +698,7 @@ export function DeployModal({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Undeploy API</ModalHeader> <ModalHeader>Undeploy API</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to undeploy this workflow?{' '} Are you sure you want to undeploy this workflow?{' '}
<span className='text-[var(--text-error)]'> <span className='text-[var(--text-error)]'>
This will remove the API endpoint and make it unavailable to external users. This will remove the API endpoint and make it unavailable to external users.
@@ -713,12 +713,7 @@ export function DeployModal({
> >
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={handleUndeploy} disabled={isUndeploying}>
variant='primary'
onClick={handleUndeploy}
disabled={isUndeploying}
className='bg-[var(--text-error)] text-[13px] text-white hover:bg-[var(--text-error)]'
>
{isUndeploying ? 'Undeploying...' : 'Undeploy'} {isUndeploying ? 'Undeploying...' : 'Undeploy'}
</Button> </Button>
</ModalFooter> </ModalFooter>
@@ -734,29 +729,10 @@ interface StatusBadgeProps {
function StatusBadge({ isWarning }: StatusBadgeProps) { function StatusBadge({ isWarning }: StatusBadgeProps) {
const label = isWarning ? 'Update deployment' : 'Live' const label = isWarning ? 'Update deployment' : 'Live'
return ( return (
<div <Badge variant={isWarning ? 'amber' : 'green'} size='lg' dot>
className={clsx( {label}
'flex h-[24px] items-center justify-start gap-[8px] rounded-[6px] border px-[9px]', </Badge>
isWarning ? 'border-[#A16207] bg-[#452C0F]' : 'border-[#22703D] bg-[#14291B]'
)}
>
<div
className='h-[6px] w-[6px] rounded-[2px]'
style={{
backgroundColor: isWarning ? '#EAB308' : '#4ADE80',
}}
/>
<span
className='font-medium text-[11.5px]'
style={{
color: isWarning ? '#EAB308' : '#86EFAC',
}}
>
{label}
</span>
</div>
) )
} }
@@ -776,35 +752,10 @@ function TemplateStatusBadge({ status, views, stars }: TemplateStatusBadgeProps)
: null : null
return ( return (
<div <Badge variant={isPending ? 'amber' : 'green'} size='lg' dot>
className={clsx( {label}
'flex h-[24px] items-center justify-start gap-[8px] rounded-[6px] border px-[9px]', {statsText && <span> {statsText}</span>}
isPending ? 'border-[#A16207] bg-[#452C0F]' : 'border-[#22703D] bg-[#14291B]' </Badge>
)}
>
<div
className='h-[6px] w-[6px] rounded-[2px]'
style={{
backgroundColor: isPending ? '#EAB308' : '#4ADE80',
}}
/>
<span
className='font-medium text-[11.5px]'
style={{
color: isPending ? '#EAB308' : '#86EFAC',
}}
>
{label}
</span>
{statsText && (
<span
className='font-medium text-[11.5px]'
style={{ color: isPending ? '#EAB308' : '#86EFAC' }}
>
{statsText}
</span>
)}
</div>
) )
} }
@@ -830,8 +781,8 @@ function GeneralFooter({
if (!isDeployed) { if (!isDeployed) {
return ( return (
<ModalFooter> <ModalFooter>
<Button variant='primary' onClick={onDeploy} disabled={isSubmitting}> <Button variant='tertiary' onClick={onDeploy} disabled={isSubmitting}>
{isSubmitting ? 'Deploying...' : 'Deploy API'} {isSubmitting ? 'Deploying...' : 'Deploy'}
</Button> </Button>
</ModalFooter> </ModalFooter>
) )
@@ -845,7 +796,7 @@ function GeneralFooter({
{isUndeploying ? 'Undeploying...' : 'Undeploy'} {isUndeploying ? 'Undeploying...' : 'Undeploy'}
</Button> </Button>
{needsRedeployment && ( {needsRedeployment && (
<Button variant='primary' onClick={onRedeploy} disabled={isSubmitting || isUndeploying}> <Button variant='tertiary' onClick={onRedeploy} disabled={isSubmitting || isUndeploying}>
{isSubmitting ? 'Updating...' : 'Update'} {isSubmitting ? 'Updating...' : 'Update'}
</Button> </Button>
)} )}

View File

@@ -2,7 +2,7 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { Loader2 } from 'lucide-react' import { Loader2 } from 'lucide-react'
import { Button, Rocket, Tooltip } from '@/components/emcn' import { Button, Tooltip } from '@/components/emcn'
import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal' import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal'
import { import {
useChangeDetection, useChangeDetection,
@@ -112,17 +112,15 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<span> <span>
<Button <Button
className='h-[32px] gap-[8px] px-[10px]' className='h-[30px] gap-[6px] px-[10px]'
variant='active' variant={
isRegistryLoading ? 'active' : changeDetected || !isDeployed ? 'tertiary' : 'active'
}
onClick={onDeployClick} onClick={onDeployClick}
disabled={isDisabled} disabled={isRegistryLoading || isDisabled}
> >
{isDeploying ? ( {isDeploying && <Loader2 className='h-[13px] w-[13px] animate-spin' />}
<Loader2 className='h-[13px] w-[13px] animate-spin' /> {changeDetected ? 'Update' : isDeployed ? 'Live' : 'Deploy'}
) : (
<Rocket className='h-[13px] w-[13px]' />
)}
{changeDetected ? 'Update' : isDeployed ? 'Active' : 'Deploy'}
</Button> </Button>
</span> </span>
</Tooltip.Trigger> </Tooltip.Trigger>

View File

@@ -29,17 +29,6 @@ interface FieldItemProps {
onToggleExpand?: (path: string) => void onToggleExpand?: (path: string) => void
} }
/**
* Tree layout constants shared with parent component
*/
export const TREE_SPACING = {
INDENT_PER_LEVEL: 20,
BASE_INDENT: 20,
VERTICAL_LINE_LEFT_OFFSET: 4,
ITEM_GAP: 0,
ITEM_HEIGHT: 25,
} as const
/** /**
* Individual field item component with drag functionality * Individual field item component with drag functionality
*/ */
@@ -52,8 +41,6 @@ export function FieldItem({
isExpanded, isExpanded,
onToggleExpand, onToggleExpand,
}: FieldItemProps) { }: FieldItemProps) {
const indent = TREE_SPACING.BASE_INDENT + level * TREE_SPACING.INDENT_PER_LEVEL
const handleDragStart = useCallback( const handleDragStart = useCallback(
(e: React.DragEvent) => { (e: React.DragEvent) => {
const normalizedBlockName = connection.name.replace(/\s+/g, '').toLowerCase() const normalizedBlockName = connection.name.replace(/\s+/g, '').toLowerCase()
@@ -91,25 +78,26 @@ export function FieldItem({
onDragStart={handleDragStart} onDragStart={handleDragStart}
onClick={handleClick} onClick={handleClick}
className={clsx( className={clsx(
'group flex h-[25px] cursor-grab items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--border)] active:cursor-grabbing', 'group flex h-[26px] cursor-grab items-center gap-[8px] rounded-[8px] px-[6px] text-[14px] hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
hasChildren && 'cursor-pointer' hasChildren && 'cursor-pointer'
)} )}
style={{ marginLeft: `${indent}px` }}
> >
<span <span
className={clsx( className={clsx(
'flex-1 truncate font-medium', 'min-w-0 flex-1 truncate font-medium',
'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]' 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]'
)} )}
> >
{field.name} {field.name}
</span> </span>
<Badge className='rounded-[2px] px-[4px] py-[1px] font-mono text-[10px]'>{field.type}</Badge> <Badge className='flex-shrink-0 rounded-[4px] px-[6px] py-[1px] font-mono text-[11px]'>
{field.type}
</Badge>
{hasChildren && ( {hasChildren && (
<ChevronDown <ChevronDown
className={clsx( className={clsx(
'h-3.5 w-3.5 flex-shrink-0 transition-transform', 'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-100',
'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]', 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]',
isExpanded && 'rotate-180' isExpanded && 'rotate-180'
)} )}
/> />

View File

@@ -8,7 +8,6 @@ import { useShallow } from 'zustand/react/shallow'
import { import {
FieldItem, FieldItem,
type SchemaField, type SchemaField,
TREE_SPACING,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item' } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item'
import type { ConnectedBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections' import type { ConnectedBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections'
import { useBlockOutputFields } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields' import { useBlockOutputFields } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields'
@@ -24,43 +23,6 @@ interface ConnectionBlocksProps {
currentBlockId: string currentBlockId: string
} }
const TREE_STYLES = {
LINE_COLOR: 'var(--border)',
LINE_OFFSET: 4,
} as const
/**
* Calculates total height of visible nested fields recursively
*/
const calculateFieldsHeight = (
fields: SchemaField[] | undefined,
parentPath: string,
connectionId: string,
isExpanded: (connectionId: string, path: string) => boolean
): number => {
if (!fields || fields.length === 0) return 0
let totalHeight = 0
fields.forEach((field, index) => {
const fieldPath = parentPath ? `${parentPath}.${field.name}` : field.name
const expanded = isExpanded(connectionId, fieldPath)
totalHeight += TREE_SPACING.ITEM_HEIGHT
if (index < fields.length - 1) {
totalHeight += TREE_SPACING.ITEM_GAP
}
if (expanded && field.children && field.children.length > 0) {
totalHeight += TREE_SPACING.ITEM_GAP
totalHeight += calculateFieldsHeight(field.children, fieldPath, connectionId, isExpanded)
}
})
return totalHeight
}
interface ConnectionItemProps { interface ConnectionItemProps {
connection: ConnectedBlock connection: ConnectedBlock
isExpanded: boolean isExpanded: boolean
@@ -123,13 +85,13 @@ function ConnectionItem({
draggable draggable
onDragStart={(e) => onConnectionDragStart(e, connection)} onDragStart={(e) => onConnectionDragStart(e, connection)}
className={clsx( className={clsx(
'group flex h-[25px] cursor-grab items-center gap-[8px] rounded-[8px] px-[5.5px] text-[14px] hover:bg-[var(--border)] active:cursor-grabbing', 'group flex h-[26px] cursor-grab items-center gap-[8px] rounded-[8px] px-[6px] text-[14px] hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
hasFields && 'cursor-pointer' hasFields && 'cursor-pointer'
)} )}
onClick={() => hasFields && onToggleExpand(connection.id)} onClick={() => hasFields && onToggleExpand(connection.id)}
> >
<div <div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]' className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ background: bgColor }} style={{ background: bgColor }}
> >
{Icon && ( {Icon && (
@@ -137,7 +99,7 @@ function ConnectionItem({
className={clsx( className={clsx(
'text-white transition-transform duration-200', 'text-white transition-transform duration-200',
hasFields && 'group-hover:scale-110', hasFields && 'group-hover:scale-110',
'!h-[10px] !w-[10px]' '!h-[9px] !w-[9px]'
)} )}
/> />
)} )}
@@ -145,7 +107,7 @@ function ConnectionItem({
<span <span
className={clsx( className={clsx(
'truncate font-medium', 'truncate font-medium',
'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]' 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]'
)} )}
> >
{connection.name} {connection.name}
@@ -153,8 +115,8 @@ function ConnectionItem({
{hasFields && ( {hasFields && (
<ChevronDown <ChevronDown
className={clsx( className={clsx(
'h-3.5 w-3.5 flex-shrink-0 transition-transform', 'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-100',
'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]', 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]',
isExpanded && 'rotate-180' isExpanded && 'rotate-180'
)} )}
/> />
@@ -162,17 +124,8 @@ function ConnectionItem({
</div> </div>
{isExpanded && hasFields && ( {isExpanded && hasFields && (
<div className='relative'> <div className='relative mt-[2px] ml-[12px] space-y-[2px] pl-[10px]'>
<div <div className='pointer-events-none absolute top-[4px] bottom-[4px] left-0 w-px bg-[var(--border)]' />
className='pointer-events-none absolute'
style={{
left: `${TREE_SPACING.VERTICAL_LINE_LEFT_OFFSET}px`,
top: `${TREE_STYLES.LINE_OFFSET}px`,
width: '1px',
height: `${calculateFieldsHeight(fields, '', connection.id, isFieldExpanded) - TREE_STYLES.LINE_OFFSET * 2}px`,
background: TREE_STYLES.LINE_COLOR,
}}
/>
{renderFieldTree(fields, '', 0, connection)} {renderFieldTree(fields, '', 0, connection)}
</div> </div>
)} )}
@@ -311,18 +264,9 @@ export function ConnectionBlocks({ connections, currentBlockId }: ConnectionBloc
onToggleExpand={(p) => toggleFieldExpansion(connection.id, p)} onToggleExpand={(p) => toggleFieldExpansion(connection.id, p)}
/> />
{hasChildren && expanded && ( {hasChildren && expanded && (
<div className='relative'> <div className='relative mt-[2px] ml-[6px] space-y-[2px] pl-[10px]'>
<div <div className='pointer-events-none absolute top-[4px] bottom-[4px] left-0 w-px bg-[var(--border)]' />
className='pointer-events-none absolute' {renderFieldTree(field.children!, fieldPath, level + 1, connection)}
style={{
left: `${TREE_SPACING.BASE_INDENT + level * TREE_SPACING.INDENT_PER_LEVEL + TREE_SPACING.VERTICAL_LINE_LEFT_OFFSET}px`,
top: `${TREE_STYLES.LINE_OFFSET}px`,
width: '1px',
height: `${calculateFieldsHeight(field.children, fieldPath, connection.id, isFieldExpanded) - TREE_STYLES.LINE_OFFSET * 2}px`,
background: TREE_STYLES.LINE_COLOR,
}}
/>
<div>{renderFieldTree(field.children!, fieldPath, level + 1, connection)}</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -285,6 +285,7 @@ export function ComboBox({
overlayContent={overlayContent} overlayContent={overlayContent}
inputRef={ref as React.RefObject<HTMLInputElement>} inputRef={ref as React.RefObject<HTMLInputElement>}
filterOptions filterOptions
searchable={config.searchable}
className={cn('allow-scroll overflow-x-auto', selectedOptionIcon && 'pl-[28px]')} className={cn('allow-scroll overflow-x-auto', selectedOptionIcon && 'pl-[28px]')}
inputProps={{ inputProps={{
onDrop: onDrop as (e: React.DragEvent<HTMLInputElement>) => void, onDrop: onDrop as (e: React.DragEvent<HTMLInputElement>) => void,

View File

@@ -709,14 +709,14 @@ export function ConditionInput({
{conditionalBlocks.map((block, index) => ( {conditionalBlocks.map((block, index) => (
<div <div
key={block.id} key={block.id}
className='group relative overflow-visible rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]' className='group relative overflow-visible rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]'
> >
<div <div
className={cn( className={cn(
'flex items-center justify-between overflow-hidden bg-transparent px-[10px] py-[5px]', 'flex items-center justify-between overflow-hidden bg-transparent px-[10px] py-[5px]',
block.title === 'else' block.title === 'else'
? 'rounded-[4px] border-0' ? 'rounded-[4px] border-0'
: 'rounded-t-[4px] border-[var(--border-strong)] border-b' : 'rounded-t-[4px] border-[var(--border-1)] border-b'
)} )}
> >
<span className='font-medium text-[14px] text-[var(--text-tertiary)]'> <span className='font-medium text-[14px] text-[var(--text-tertiary)]'>

View File

@@ -25,15 +25,15 @@ export interface OAuthRequiredModalProps {
} }
const SCOPE_DESCRIPTIONS: Record<string, string> = { const SCOPE_DESCRIPTIONS: Record<string, string> = {
'https://www.googleapis.com/auth/gmail.send': 'Send emails on your behalf', 'https://www.googleapis.com/auth/gmail.send': 'Send emails',
'https://www.googleapis.com/auth/gmail.labels': 'View and manage your email labels', 'https://www.googleapis.com/auth/gmail.labels': 'View and manage email labels',
'https://www.googleapis.com/auth/gmail.modify': 'View and manage your email messages', 'https://www.googleapis.com/auth/gmail.modify': 'View and manage email messages',
'https://www.googleapis.com/auth/drive.file': 'View and manage your Google Drive files', 'https://www.googleapis.com/auth/drive.file': 'View and manage Google Drive files',
'https://www.googleapis.com/auth/drive': 'Full access to all your Google Drive files', 'https://www.googleapis.com/auth/drive': 'Access all Google Drive files',
'https://www.googleapis.com/auth/calendar': 'View and manage your calendar', 'https://www.googleapis.com/auth/calendar': 'View and manage calendar',
'https://www.googleapis.com/auth/userinfo.email': 'View your email address', 'https://www.googleapis.com/auth/userinfo.email': 'View email address',
'https://www.googleapis.com/auth/userinfo.profile': 'View your basic profile info', 'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info',
'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to your Google Forms', 'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms',
'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery', 'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery',
'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage', 'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage',
'https://www.googleapis.com/auth/admin.directory.group': 'Manage Google Workspace groups', 'https://www.googleapis.com/auth/admin.directory.group': 'Manage Google Workspace groups',
@@ -65,50 +65,50 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'write:label:confluence': 'Add and remove labels', 'write:label:confluence': 'Add and remove labels',
'search:confluence': 'Search Confluence content', 'search:confluence': 'Search Confluence content',
'readonly:content.attachment:confluence': 'View attachments', 'readonly:content.attachment:confluence': 'View attachments',
'read:me': 'Read your profile information', 'read:me': 'Read profile information',
'database.read': 'Read your database', 'database.read': 'Read database',
'database.write': 'Write to your database', 'database.write': 'Write to database',
'projects.read': 'Read your projects', 'projects.read': 'Read projects',
offline_access: 'Access your account when you are not using the application', offline_access: 'Access account when not using the application',
repo: 'Access your repositories', repo: 'Access repositories',
workflow: 'Manage repository workflows', workflow: 'Manage repository workflows',
'read:user': 'Read your public user information', 'read:user': 'Read public user information',
'user:email': 'Access your email address', 'user:email': 'Access email address',
'tweet.read': 'Read your tweets and timeline', 'tweet.read': 'Read tweets and timeline',
'tweet.write': 'Post tweets on your behalf', 'tweet.write': 'Post tweets',
'users.read': 'Read your profile information', 'users.read': 'Read profile information',
'offline.access': 'Access your account when you are not using the application', 'offline.access': 'Access account when not using the application',
'data.records:read': 'Read your records', 'data.records:read': 'Read records',
'data.records:write': 'Write to your records', 'data.records:write': 'Write to records',
'webhook:manage': 'Manage your webhooks', 'webhook:manage': 'Manage webhooks',
'page.read': 'Read your Notion pages', 'page.read': 'Read Notion pages',
'page.write': 'Write to your Notion pages', 'page.write': 'Write to Notion pages',
'workspace.content': 'Read your Notion content', 'workspace.content': 'Read Notion content',
'workspace.name': 'Read your Notion workspace name', 'workspace.name': 'Read Notion workspace name',
'workspace.read': 'Read your Notion workspace', 'workspace.read': 'Read Notion workspace',
'workspace.write': 'Write to your Notion workspace', 'workspace.write': 'Write to Notion workspace',
'user.email:read': 'Read your email address', 'user.email:read': 'Read email address',
'read:jira-user': 'Read your Jira user', 'read:jira-user': 'Read Jira user',
'read:jira-work': 'Read your Jira work', 'read:jira-work': 'Read Jira work',
'write:jira-work': 'Write to your Jira work', 'write:jira-work': 'Write to Jira work',
'manage:jira-webhook': 'Register and manage Jira webhooks', 'manage:jira-webhook': 'Register and manage Jira webhooks',
'read:webhook:jira': 'View Jira webhooks', 'read:webhook:jira': 'View Jira webhooks',
'write:webhook:jira': 'Create and update Jira webhooks', 'write:webhook:jira': 'Create and update Jira webhooks',
'delete:webhook:jira': 'Delete Jira webhooks', 'delete:webhook:jira': 'Delete Jira webhooks',
'read:issue-event:jira': 'Read your Jira issue events', 'read:issue-event:jira': 'Read Jira issue events',
'write:issue:jira': 'Write to your Jira issues', 'write:issue:jira': 'Write to Jira issues',
'read:project:jira': 'Read your Jira projects', 'read:project:jira': 'Read Jira projects',
'read:issue-type:jira': 'Read your Jira issue types', 'read:issue-type:jira': 'Read Jira issue types',
'read:issue-meta:jira': 'Read your Jira issue meta', 'read:issue-meta:jira': 'Read Jira issue meta',
'read:issue-security-level:jira': 'Read your Jira issue security level', 'read:issue-security-level:jira': 'Read Jira issue security level',
'read:issue.vote:jira': 'Read your Jira issue votes', 'read:issue.vote:jira': 'Read Jira issue votes',
'read:issue.changelog:jira': 'Read your Jira issue changelog', 'read:issue.changelog:jira': 'Read Jira issue changelog',
'read:avatar:jira': 'Read your Jira avatar', 'read:avatar:jira': 'Read Jira avatar',
'read:issue:jira': 'Read your Jira issues', 'read:issue:jira': 'Read Jira issues',
'read:status:jira': 'Read your Jira status', 'read:status:jira': 'Read Jira status',
'read:user:jira': 'Read your Jira user', 'read:user:jira': 'Read Jira user',
'read:field-configuration:jira': 'Read your Jira field configuration', 'read:field-configuration:jira': 'Read Jira field configuration',
'read:issue-details:jira': 'Read your Jira issue details', 'read:issue-details:jira': 'Read Jira issue details',
'read:field:jira': 'Read Jira field configurations', 'read:field:jira': 'Read Jira field configurations',
'read:jql:jira': 'Use JQL to filter Jira issues', 'read:jql:jira': 'Use JQL to filter Jira issues',
'read:comment.property:jira': 'Read Jira comment properties', 'read:comment.property:jira': 'Read Jira comment properties',
@@ -124,57 +124,57 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'delete:issue-worklog:jira': 'Delete worklog entries from Jira issues', 'delete:issue-worklog:jira': 'Delete worklog entries from Jira issues',
'write:issue-link:jira': 'Create links between Jira issues', 'write:issue-link:jira': 'Create links between Jira issues',
'delete:issue-link:jira': 'Delete links between Jira issues', 'delete:issue-link:jira': 'Delete links between Jira issues',
'User.Read': 'Read your Microsoft user', 'User.Read': 'Read Microsoft user',
'Chat.Read': 'Read your Microsoft chats', 'Chat.Read': 'Read Microsoft chats',
'Chat.ReadWrite': 'Write to your Microsoft chats', 'Chat.ReadWrite': 'Write to Microsoft chats',
'Chat.ReadBasic': 'Read your Microsoft chats', 'Chat.ReadBasic': 'Read Microsoft chats',
'ChatMessage.Send': 'Send chat messages on your behalf', 'ChatMessage.Send': 'Send chat messages',
'Channel.ReadBasic.All': 'Read your Microsoft channels', 'Channel.ReadBasic.All': 'Read Microsoft channels',
'ChannelMessage.Send': 'Write to your Microsoft channels', 'ChannelMessage.Send': 'Write to Microsoft channels',
'ChannelMessage.Read.All': 'Read your Microsoft channels', 'ChannelMessage.Read.All': 'Read Microsoft channels',
'ChannelMessage.ReadWrite': 'Read and write to your Microsoft channels', 'ChannelMessage.ReadWrite': 'Read and write to Microsoft channels',
'ChannelMember.Read.All': 'Read team channel members', 'ChannelMember.Read.All': 'Read team channel members',
'Group.Read.All': 'Read your Microsoft groups', 'Group.Read.All': 'Read Microsoft groups',
'Group.ReadWrite.All': 'Write to your Microsoft groups', 'Group.ReadWrite.All': 'Write to Microsoft groups',
'Team.ReadBasic.All': 'Read your Microsoft teams', 'Team.ReadBasic.All': 'Read Microsoft teams',
'TeamMember.Read.All': 'Read team members', 'TeamMember.Read.All': 'Read team members',
'Mail.ReadWrite': 'Write to your Microsoft emails', 'Mail.ReadWrite': 'Write to Microsoft emails',
'Mail.ReadBasic': 'Read your Microsoft emails', 'Mail.ReadBasic': 'Read Microsoft emails',
'Mail.Read': 'Read your Microsoft emails', 'Mail.Read': 'Read Microsoft emails',
'Mail.Send': 'Send emails on your behalf', 'Mail.Send': 'Send emails',
'Files.Read': 'Read your OneDrive files', 'Files.Read': 'Read OneDrive files',
'Files.ReadWrite': 'Read and write your OneDrive files', 'Files.ReadWrite': 'Read and write OneDrive files',
'Tasks.ReadWrite': 'Read and manage your Planner tasks', 'Tasks.ReadWrite': 'Read and manage Planner tasks',
'Sites.Read.All': 'Read Sharepoint sites', 'Sites.Read.All': 'Read Sharepoint sites',
'Sites.ReadWrite.All': 'Read and write Sharepoint sites', 'Sites.ReadWrite.All': 'Read and write Sharepoint sites',
'Sites.Manage.All': 'Manage Sharepoint sites', 'Sites.Manage.All': 'Manage Sharepoint sites',
openid: 'Standard authentication', openid: 'Standard authentication',
profile: 'Access your profile information', profile: 'Access profile information',
email: 'Access your email address', email: 'Access email address',
identify: 'Read your Discord user', identify: 'Read Discord user',
bot: 'Read your Discord bot', bot: 'Read Discord bot',
'messages.read': 'Read your Discord messages', 'messages.read': 'Read Discord messages',
guilds: 'Read your Discord guilds', guilds: 'Read Discord guilds',
'guilds.members.read': 'Read your Discord guild members', 'guilds.members.read': 'Read Discord guild members',
identity: 'Access your Reddit identity', identity: 'Access Reddit identity',
submit: 'Submit posts and comments on your behalf', submit: 'Submit posts and comments',
vote: 'Vote on posts and comments', vote: 'Vote on posts and comments',
save: 'Save and unsave posts and comments', save: 'Save and unsave posts and comments',
edit: 'Edit your posts and comments', edit: 'Edit posts and comments',
subscribe: 'Subscribe and unsubscribe from subreddits', subscribe: 'Subscribe and unsubscribe from subreddits',
history: 'Access your Reddit history', history: 'Access Reddit history',
privatemessages: 'Access your inbox and send private messages', privatemessages: 'Access inbox and send private messages',
account: 'Update your account preferences and settings', account: 'Update account preferences and settings',
mysubreddits: 'Access your subscribed and moderated subreddits', mysubreddits: 'Access subscribed and moderated subreddits',
flair: 'Manage user and post flair', flair: 'Manage user and post flair',
report: 'Report posts and comments for rule violations', report: 'Report posts and comments for rule violations',
modposts: 'Approve, remove, and moderate posts in subreddits you moderate', modposts: 'Approve, remove, and moderate posts in moderated subreddits',
modflair: 'Manage flair in subreddits you moderate', modflair: 'Manage flair in moderated subreddits',
modmail: 'Access and respond to moderator mail', modmail: 'Access and respond to moderator mail',
login: 'Access your Wealthbox account', login: 'Access Wealthbox account',
data: 'Access your Wealthbox data', data: 'Access Wealthbox data',
read: 'Read access to your workspace', read: 'Read access to workspace',
write: 'Write access to your Linear workspace', write: 'Write access to Linear workspace',
'channels:read': 'View public channels', 'channels:read': 'View public channels',
'channels:history': 'Read channel messages', 'channels:history': 'Read channel messages',
'groups:read': 'View private channels', 'groups:read': 'View private channels',
@@ -189,15 +189,15 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'files:read': 'Download and read files', 'files:read': 'Download and read files',
'canvases:write': 'Create canvas documents', 'canvases:write': 'Create canvas documents',
'reactions:write': 'Add emoji reactions to messages', 'reactions:write': 'Add emoji reactions to messages',
'sites:read': 'View your Webflow sites', 'sites:read': 'View Webflow sites',
'sites:write': 'Manage webhooks and site settings', 'sites:write': 'Manage webhooks and site settings',
'cms:read': 'View your CMS content', 'cms:read': 'View CMS content',
'cms:write': 'Manage your CMS content', 'cms:write': 'Manage CMS content',
'crm.objects.contacts.read': 'Read your HubSpot contacts', 'crm.objects.contacts.read': 'Read HubSpot contacts',
'crm.objects.contacts.write': 'Create and update HubSpot contacts', 'crm.objects.contacts.write': 'Create and update HubSpot contacts',
'crm.objects.companies.read': 'Read your HubSpot companies', 'crm.objects.companies.read': 'Read HubSpot companies',
'crm.objects.companies.write': 'Create and update HubSpot companies', 'crm.objects.companies.write': 'Create and update HubSpot companies',
'crm.objects.deals.read': 'Read your HubSpot deals', 'crm.objects.deals.read': 'Read HubSpot deals',
'crm.objects.deals.write': 'Create and update HubSpot deals', 'crm.objects.deals.write': 'Create and update HubSpot deals',
'crm.objects.owners.read': 'Read HubSpot object owners', 'crm.objects.owners.read': 'Read HubSpot object owners',
'crm.objects.users.read': 'Read HubSpot users', 'crm.objects.users.read': 'Read HubSpot users',
@@ -217,74 +217,74 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'crm.lists.write': 'Create and update HubSpot lists', 'crm.lists.write': 'Create and update HubSpot lists',
tickets: 'Manage HubSpot tickets', tickets: 'Manage HubSpot tickets',
api: 'Access Salesforce API', api: 'Access Salesforce API',
refresh_token: 'Maintain long-term access to your Salesforce account', refresh_token: 'Maintain long-term access to Salesforce account',
default: 'Access your Asana workspace', default: 'Access Asana workspace',
base: 'Basic access to your Pipedrive account', base: 'Basic access to Pipedrive account',
'deals:read': 'Read your Pipedrive deals', 'deals:read': 'Read Pipedrive deals',
'deals:full': 'Full access to manage your Pipedrive deals', 'deals:full': 'Full access to manage Pipedrive deals',
'contacts:read': 'Read your Pipedrive contacts', 'contacts:read': 'Read Pipedrive contacts',
'contacts:full': 'Full access to manage your Pipedrive contacts', 'contacts:full': 'Full access to manage Pipedrive contacts',
'leads:read': 'Read your Pipedrive leads', 'leads:read': 'Read Pipedrive leads',
'leads:full': 'Full access to manage your Pipedrive leads', 'leads:full': 'Full access to manage Pipedrive leads',
'activities:read': 'Read your Pipedrive activities', 'activities:read': 'Read Pipedrive activities',
'activities:full': 'Full access to manage your Pipedrive activities', 'activities:full': 'Full access to manage Pipedrive activities',
'mail:read': 'Read your Pipedrive emails', 'mail:read': 'Read Pipedrive emails',
'mail:full': 'Full access to manage your Pipedrive emails', 'mail:full': 'Full access to manage Pipedrive emails',
'projects:read': 'Read your Pipedrive projects', 'projects:read': 'Read Pipedrive projects',
'projects:full': 'Full access to manage your Pipedrive projects', 'projects:full': 'Full access to manage Pipedrive projects',
'webhooks:read': 'Read your Pipedrive webhooks', 'webhooks:read': 'Read Pipedrive webhooks',
'webhooks:full': 'Full access to manage your Pipedrive webhooks', 'webhooks:full': 'Full access to manage Pipedrive webhooks',
w_member_social: 'Access your LinkedIn profile', w_member_social: 'Access LinkedIn profile',
// Box scopes // Box scopes
root_readwrite: 'Read and write all files and folders in your Box account', root_readwrite: 'Read and write all files and folders in Box account',
root_readonly: 'Read all files and folders in your Box account', root_readonly: 'Read all files and folders in Box account',
// Shopify scopes (write_* implicitly includes read access) // Shopify scopes (write_* implicitly includes read access)
write_products: 'Read and manage your Shopify products', write_products: 'Read and manage Shopify products',
write_orders: 'Read and manage your Shopify orders', write_orders: 'Read and manage Shopify orders',
write_customers: 'Read and manage your Shopify customers', write_customers: 'Read and manage Shopify customers',
write_inventory: 'Read and manage your Shopify inventory levels', write_inventory: 'Read and manage Shopify inventory levels',
read_locations: 'View your store locations', read_locations: 'View store locations',
write_merchant_managed_fulfillment_orders: 'Create fulfillments for orders', write_merchant_managed_fulfillment_orders: 'Create fulfillments for orders',
// Zoom scopes // Zoom scopes
'user:read:user': 'View your Zoom profile information', 'user:read:user': 'View Zoom profile information',
'meeting:write:meeting': 'Create Zoom meetings', 'meeting:write:meeting': 'Create Zoom meetings',
'meeting:read:meeting': 'View Zoom meeting details', 'meeting:read:meeting': 'View Zoom meeting details',
'meeting:read:list_meetings': 'List your Zoom meetings', 'meeting:read:list_meetings': 'List Zoom meetings',
'meeting:update:meeting': 'Update Zoom meetings', 'meeting:update:meeting': 'Update Zoom meetings',
'meeting:delete:meeting': 'Delete Zoom meetings', 'meeting:delete:meeting': 'Delete Zoom meetings',
'meeting:read:invitation': 'View Zoom meeting invitations', 'meeting:read:invitation': 'View Zoom meeting invitations',
'meeting:read:list_past_participants': 'View past meeting participants', 'meeting:read:list_past_participants': 'View past meeting participants',
'cloud_recording:read:list_user_recordings': 'List your Zoom cloud recordings', 'cloud_recording:read:list_user_recordings': 'List Zoom cloud recordings',
'cloud_recording:read:list_recording_files': 'View recording files', 'cloud_recording:read:list_recording_files': 'View recording files',
'cloud_recording:delete:recording_file': 'Delete cloud recordings', 'cloud_recording:delete:recording_file': 'Delete cloud recordings',
// Dropbox scopes // Dropbox scopes
'account_info.read': 'View your Dropbox account information', 'account_info.read': 'View Dropbox account information',
'files.metadata.read': 'View file and folder names, sizes, and dates', 'files.metadata.read': 'View file and folder names, sizes, and dates',
'files.metadata.write': 'Modify file and folder metadata', 'files.metadata.write': 'Modify file and folder metadata',
'files.content.read': 'Download and read your Dropbox files', 'files.content.read': 'Download and read Dropbox files',
'files.content.write': 'Upload, copy, move, and delete files in your Dropbox', 'files.content.write': 'Upload, copy, move, and delete files in Dropbox',
'sharing.read': 'View your shared files and folders', 'sharing.read': 'View shared files and folders',
'sharing.write': 'Share files and folders with others', 'sharing.write': 'Share files and folders with others',
// WordPress.com scopes // WordPress.com scopes
global: 'Full access to manage your WordPress.com sites, posts, pages, media, and settings', global: 'Full access to manage WordPress.com sites, posts, pages, media, and settings',
// Spotify scopes // Spotify scopes
'user-read-private': 'View your Spotify account details', 'user-read-private': 'View Spotify account details',
'user-read-email': 'View your email address on Spotify', 'user-read-email': 'View email address on Spotify',
'user-library-read': 'View your saved tracks and albums', 'user-library-read': 'View saved tracks and albums',
'user-library-modify': 'Save and remove tracks and albums from your library', 'user-library-modify': 'Save and remove tracks and albums from library',
'playlist-read-private': 'View your private playlists', 'playlist-read-private': 'View private playlists',
'playlist-read-collaborative': 'View collaborative playlists you have access to', 'playlist-read-collaborative': 'View collaborative playlists',
'playlist-modify-public': 'Create and manage your public playlists', 'playlist-modify-public': 'Create and manage public playlists',
'playlist-modify-private': 'Create and manage your private playlists', 'playlist-modify-private': 'Create and manage private playlists',
'user-read-playback-state': 'View your current playback state', 'user-read-playback-state': 'View current playback state',
'user-modify-playback-state': 'Control playback on your Spotify devices', 'user-modify-playback-state': 'Control playback on Spotify devices',
'user-read-currently-playing': 'View your currently playing track', 'user-read-currently-playing': 'View currently playing track',
'user-read-recently-played': 'View your recently played tracks', 'user-read-recently-played': 'View recently played tracks',
'user-top-read': 'View your top artists and tracks', 'user-top-read': 'View top artists and tracks',
'user-follow-read': 'View artists and users you follow', 'user-follow-read': 'View followed artists and users',
'user-follow-modify': 'Follow and unfollow artists and users', 'user-follow-modify': 'Follow and unfollow artists and users',
'user-read-playback-position': 'View your playback position in podcasts', 'user-read-playback-position': 'View playback position in podcasts',
'ugc-image-upload': 'Upload images to your Spotify playlists', 'ugc-image-upload': 'Upload images to Spotify playlists',
} }
function getScopeDescription(scope: string): string { function getScopeDescription(scope: string): string {
@@ -378,7 +378,7 @@ export function OAuthRequiredModal({
<ModalBody> <ModalBody>
<div className='flex flex-col gap-[16px]'> <div className='flex flex-col gap-[16px]'>
<div className='flex items-center gap-[14px]'> <div className='flex items-center gap-[14px]'>
<div className='flex h-[40px] w-[40px] flex-shrink-0 items-center justify-center rounded-[8px] bg-[var(--surface-6)]'> <div className='flex h-[40px] w-[40px] flex-shrink-0 items-center justify-center rounded-[8px] bg-[var(--surface-5)]'>
<ProviderIcon className='h-[18px] w-[18px]' /> <ProviderIcon className='h-[18px] w-[18px]' />
</div> </div>
<div className='flex-1'> <div className='flex-1'>
@@ -392,22 +392,22 @@ export function OAuthRequiredModal({
</div> </div>
{displayScopes.length > 0 && ( {displayScopes.length > 0 && (
<div className='rounded-[8px] border bg-[var(--surface-6)]'> <div className='rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-5)]'>
<div className='border-b px-[14px] py-[10px]'> <div className='border-[var(--border-1)] border-b px-[14px] py-[10px]'>
<h4 className='font-medium text-[13px] text-[var(--text-primary)]'> <h4 className='font-medium text-[12px] text-[var(--text-primary)]'>
Permissions requested Permissions requested
</h4> </h4>
</div> </div>
<ul className='max-h-[330px] space-y-[10px] overflow-y-auto px-[14px] py-[12px]'> <ul className='max-h-[330px] space-y-[10px] overflow-y-auto px-[14px] py-[12px]'>
{displayScopes.map((scope) => ( {displayScopes.map((scope) => (
<li key={scope} className='flex items-start gap-[10px]'> <li key={scope} className='flex items-start gap-[10px]'>
<div className='mt-[3px] flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'> <div className='mt-[2px] flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
<Check className='h-[10px] w-[10px] text-[var(--text-primary)]' /> <Check className='h-[10px] w-[10px] text-[var(--text-primary)]' />
</div> </div>
<div className='flex-1 text-[12px] text-[var(--text-primary)]'> <div className='flex flex-1 items-center gap-[8px] text-[12px] text-[var(--text-primary)]'>
<span>{getScopeDescription(scope)}</span> <span>{getScopeDescription(scope)}</span>
{newScopesSet.has(scope) && ( {newScopesSet.has(scope) && (
<span className='ml-[8px] rounded-[4px] border border-amber-500/30 bg-amber-500/10 px-[6px] py-[2px] text-[10px] text-amber-300'> <span className='inline-flex items-center gap-[6px] rounded-[6px] bg-[rgba(245,158,11,0.2)] px-[7px] py-[1px] font-medium text-[#fcd34d] text-[11px]'>
New New
</span> </span>
)} )}
@@ -423,12 +423,7 @@ export function OAuthRequiredModal({
<Button variant='active' onClick={onClose}> <Button variant='active' onClick={onClose}>
Cancel Cancel
</Button> </Button>
<Button <Button variant='tertiary' type='button' onClick={handleConnectDirectly}>
variant='primary'
type='button'
onClick={handleConnectDirectly}
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
>
Connect Connect
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -127,7 +127,7 @@ export function EvalInput({
} }
const renderMetricHeader = (metric: EvalMetric, index: number) => ( const renderMetricHeader = (metric: EvalMetric, index: number) => (
<div className='flex items-center justify-between overflow-hidden rounded-t-[4px] border-[var(--border-strong)] border-b bg-transparent px-[10px] py-[5px]'> <div className='flex items-center justify-between overflow-hidden rounded-t-[4px] border-[var(--border-1)] border-b bg-transparent px-[10px] py-[5px]'>
<span className='font-medium text-[14px] text-[var(--text-tertiary)]'> <span className='font-medium text-[14px] text-[var(--text-tertiary)]'>
Metric {index + 1} Metric {index + 1}
</span> </span>
@@ -171,11 +171,11 @@ export function EvalInput({
<div <div
key={metric.id} key={metric.id}
data-metric-id={metric.id} data-metric-id={metric.id}
className='group relative overflow-visible rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]' className='group relative overflow-visible rounded-[4px] border border-[var(--border-1)] bg-[#1F1F1F]'
> >
{renderMetricHeader(metric, index)} {renderMetricHeader(metric, index)}
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] px-[10px] pt-[6px] pb-[10px]'> <div className='flex flex-col gap-[6px] border-[var(--border-1)] px-[10px] pt-[6px] pb-[10px]'>
<div key={`name-${metric.id}`} className='space-y-[4px]'> <div key={`name-${metric.id}`} className='space-y-[4px]'>
<Label className='text-[13px]'>Name</Label> <Label className='text-[13px]'>Name</Label>
<Input <Input

View File

@@ -420,7 +420,7 @@ export function FileUpload({
return ( return (
<div <div
key={fileKey} key={fileKey}
className='flex items-center justify-between rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] hover:border-[var(--surface-14)] hover:bg-[var(--surface-9)] dark:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-11)]' className='flex items-center justify-between rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] hover:border-[var(--surface-7)] hover:bg-[var(--surface-5)] dark:bg-[var(--surface-5)] dark:hover:bg-[var(--border-1)]'
> >
<div className='flex-1 truncate pr-2 text-sm' title={file.name}> <div className='flex-1 truncate pr-2 text-sm' title={file.name}>
<span className='text-[var(--text-primary)]'>{truncateMiddle(file.name)}</span> <span className='text-[var(--text-primary)]'>{truncateMiddle(file.name)}</span>
@@ -447,7 +447,7 @@ export function FileUpload({
return ( return (
<div <div
key={file.id} key={file.id}
className='flex items-center justify-between rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] dark:bg-[var(--surface-9)]' className='flex items-center justify-between rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] dark:bg-[var(--surface-5)]'
> >
<div className='flex-1 truncate pr-2 text-sm'> <div className='flex-1 truncate pr-2 text-sm'>
<span className='text-[var(--text-primary)]'>{file.name}</span> <span className='text-[var(--text-primary)]'>{file.name}</span>

View File

@@ -107,8 +107,8 @@ export function GroupedCheckboxList({
variant='ghost' variant='ghost'
disabled={disabled} disabled={disabled}
className={cn( className={cn(
'flex w-full cursor-pointer items-center justify-between rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] font-medium font-sans text-[var(--text-primary)] text-sm outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-[var(--surface-9)]', 'flex w-full cursor-pointer items-center justify-between rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] font-medium font-sans text-[var(--text-primary)] text-sm outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-[var(--surface-5)]',
'hover:border-[var(--surface-14)] hover:bg-[var(--surface-9)] dark:hover:border-[var(--surface-13)] dark:hover:bg-[var(--surface-11)]' 'hover:border-[var(--surface-7)] hover:bg-[var(--surface-5)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]'
)} )}
> >
<span className='flex flex-1 items-center gap-2 truncate text-[var(--text-muted)]'> <span className='flex flex-1 items-center gap-2 truncate text-[var(--text-muted)]'>

View File

@@ -256,7 +256,7 @@ export function InputMapping({
if (!selectedWorkflowId) { if (!selectedWorkflowId) {
return ( return (
<div className='flex flex-col items-center justify-center rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] p-8 text-center dark:bg-[#1F1F1F]'> <div className='flex flex-col items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] p-8 text-center dark:bg-[#1F1F1F]'>
<svg <svg
className='mb-3 h-10 w-10 text-[var(--text-tertiary)]' className='mb-3 h-10 w-10 text-[var(--text-tertiary)]'
fill='none' fill='none'
@@ -368,7 +368,7 @@ function InputMappingField({
return ( return (
<div <div
className={cn( className={cn(
'rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]', 'rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]',
collapsed ? 'overflow-hidden' : 'overflow-visible' collapsed ? 'overflow-hidden' : 'overflow-visible'
)} )}
> >
@@ -385,7 +385,7 @@ function InputMappingField({
</div> </div>
{!collapsed && ( {!collapsed && (
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] border-t px-[10px] pt-[6px] pb-[10px]'> <div className='flex flex-col gap-[6px] border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='space-y-[4px]'> <div className='space-y-[4px]'>
<Label className='text-[13px]'>Value</Label> <Label className='text-[13px]'>Value</Label>
<div className='relative'> <div className='relative'>

View File

@@ -369,7 +369,7 @@ export function LongInput({
{/* Custom resize handle */} {/* Custom resize handle */}
{!wandHook.isStreaming && ( {!wandHook.isStreaming && (
<div <div
className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] dark:bg-[var(--surface-9)]' className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] dark:bg-[var(--surface-5)]'
onMouseDown={startResize} onMouseDown={startResize}
onDragStart={(e) => { onDragStart={(e) => {
e.preventDefault() e.preventDefault()

View File

@@ -313,7 +313,7 @@ export function MessagesInput({
<div <div
key={`message-${index}`} key={`message-${index}`}
className={cn( className={cn(
'relative flex w-full flex-col rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] transition-colors dark:bg-[var(--surface-9)]', 'relative flex w-full flex-col rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] transition-colors dark:bg-[var(--surface-5)]',
disabled && 'opacity-50' disabled && 'opacity-50'
)} )}
> >
@@ -364,7 +364,7 @@ export function MessagesInput({
type='button' type='button'
disabled={isPreview || disabled} disabled={isPreview || disabled}
className={cn( className={cn(
'-ml-1.5 -my-1 rounded px-1.5 py-1 font-medium text-[13px] text-[var(--text-primary)] leading-none transition-colors hover:bg-[var(--surface-8)] hover:text-[var(--text-secondary)]', '-ml-1.5 -my-1 rounded px-1.5 py-1 font-medium text-[13px] text-[var(--text-primary)] leading-none transition-colors hover:bg-[var(--surface-5)] hover:text-[var(--text-secondary)]',
(isPreview || disabled) && (isPreview || disabled) &&
'cursor-default hover:bg-transparent hover:text-[var(--text-primary)]' 'cursor-default hover:bg-transparent hover:text-[var(--text-primary)]'
)} )}
@@ -534,7 +534,7 @@ export function MessagesInput({
{!isPreview && !disabled && ( {!isPreview && !disabled && (
<div <div
className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] dark:bg-[var(--surface-9)]' className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] dark:bg-[var(--surface-5)]'
onMouseDown={(e) => handleResizeStart(fieldId, e)} onMouseDown={(e) => handleResizeStart(fieldId, e)}
onDragStart={(e) => { onDragStart={(e) => {
e.preventDefault() e.preventDefault()

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { Slider } from '@/components/ui/slider' import { Slider } from '@/components/emcn/components/slider/slider'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
interface SliderInputProps { interface SliderInputProps {
@@ -67,7 +67,6 @@ export function SliderInput({
step={integer ? 1 : step} step={integer ? 1 : step}
onValueChange={handleValueChange} onValueChange={handleValueChange}
disabled={isPreview || disabled} disabled={isPreview || disabled}
className='[&_[class*=SliderTrack]]:h-1 [&_[role=slider]]:h-4 [&_[role=slider]]:w-4 [&_[role=slider]]:cursor-pointer'
/> />
<div <div
className='absolute top-6 text-muted-foreground text-sm' className='absolute top-6 text-muted-foreground text-sm'

View File

@@ -407,14 +407,14 @@ export function FieldFormat({
key={field.id} key={field.id}
data-field-id={field.id} data-field-id={field.id}
className={cn( className={cn(
'rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]', 'rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]',
field.collapsed ? 'overflow-hidden' : 'overflow-visible' field.collapsed ? 'overflow-hidden' : 'overflow-visible'
)} )}
> >
{renderFieldHeader(field, index)} {renderFieldHeader(field, index)}
{!field.collapsed && ( {!field.collapsed && (
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] border-t px-[10px] pt-[6px] pb-[10px]'> <div className='flex flex-col gap-[6px] border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[4px]'> <div className='flex flex-col gap-[4px]'>
<Label className='text-[13px]'>Name</Label> <Label className='text-[13px]'>Name</Label>
<Input <Input

View File

@@ -148,13 +148,13 @@ export function Table({
const renderHeader = () => ( const renderHeader = () => (
<thead className='bg-transparent'> <thead className='bg-transparent'>
<tr className='border-[var(--border-strong)] border-b bg-transparent'> <tr className='border-[var(--border-1)] border-b bg-transparent'>
{columns.map((column, index) => ( {columns.map((column, index) => (
<th <th
key={column} key={column}
className={cn( className={cn(
'bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]', 'bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]',
index < columns.length - 1 && 'border-[var(--border-strong)] border-r' index < columns.length - 1 && 'border-[var(--border-1)] border-r'
)} )}
> >
{column} {column}
@@ -223,7 +223,7 @@ export function Table({
key={`${row.id}-${column}`} key={`${row.id}-${column}`}
className={cn( className={cn(
'relative bg-transparent p-0', 'relative bg-transparent p-0',
cellIndex < columns.length - 1 && 'border-[var(--border-strong)] border-r' cellIndex < columns.length - 1 && 'border-[var(--border-1)] border-r'
)} )}
> >
<div className='relative w-full'> <div className='relative w-full'>
@@ -310,14 +310,14 @@ export function Table({
return ( return (
<div className='relative'> <div className='relative'>
<div className='overflow-visible rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-2)] dark:bg-[#1F1F1F]'> <div className='overflow-visible rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-2)] dark:bg-[#1F1F1F]'>
<table className='w-full bg-transparent'> <table className='w-full bg-transparent'>
{renderHeader()} {renderHeader()}
<tbody className='bg-transparent'> <tbody className='bg-transparent'>
{rows.map((row, rowIndex) => ( {rows.map((row, rowIndex) => (
<tr <tr
key={row.id} key={row.id}
className='group relative border-[var(--border-strong)] border-t bg-transparent' className='group relative border-[var(--border-1)] border-t bg-transparent'
> >
{columns.map((column, cellIndex) => renderCell(row, rowIndex, column, cellIndex))} {columns.map((column, cellIndex) => renderCell(row, rowIndex, column, cellIndex))}
{renderDeleteButton(rowIndex)} {renderDeleteButton(rowIndex)}

View File

@@ -68,10 +68,15 @@ interface TagDropdownProps {
} }
/** /**
* Checks if the tag trigger (<) should show the tag dropdown * Checks if the tag trigger (`<`) should show the tag dropdown.
* @param text - The full text content *
* @param cursorPosition - Current cursor position * @remarks
* @returns Object indicating whether to show the dropdown * The dropdown appears when there's an unclosed `<` bracket before the cursor.
* A closing `>` bracket after the last `<` will prevent the dropdown from showing.
*
* @param text - The full text content of the input field
* @param cursorPosition - Current cursor position in the text
* @returns Object with `show` property indicating whether to display the dropdown
*/ */
export const checkTagTrigger = (text: string, cursorPosition: number): { show: boolean } => { export const checkTagTrigger = (text: string, cursorPosition: number): { show: boolean } => {
if (cursorPosition >= 1) { if (cursorPosition >= 1) {
@@ -86,6 +91,17 @@ export const checkTagTrigger = (text: string, cursorPosition: number): { show: b
return { show: false } return { show: false }
} }
/**
* Extracts the search term for tag filtering from the current input.
*
* @remarks
* Returns the text between the last unclosed `<` and the cursor position,
* converted to lowercase for case-insensitive matching.
*
* @param text - The full text content of the input field
* @param cursorPosition - Current cursor position in the text
* @returns The search term for filtering tags, or empty string if not in tag context
*/
export const getTagSearchTerm = (text: string, cursorPosition: number): string => { export const getTagSearchTerm = (text: string, cursorPosition: number): string => {
if (cursorPosition <= 0) { if (cursorPosition <= 0) {
return '' return ''
@@ -107,6 +123,9 @@ export const getTagSearchTerm = (text: string, cursorPosition: number): string =
return textBeforeCursor.slice(lastOpenBracket + 1).toLowerCase() return textBeforeCursor.slice(lastOpenBracket + 1).toLowerCase()
} }
/**
* Color constants for block type icons in the tag dropdown.
*/
const BLOCK_COLORS = { const BLOCK_COLORS = {
VARIABLE: '#2F8BFF', VARIABLE: '#2F8BFF',
DEFAULT: '#2F55FF', DEFAULT: '#2F55FF',
@@ -114,6 +133,9 @@ const BLOCK_COLORS = {
PARALLEL: '#FEE12B', PARALLEL: '#FEE12B',
} as const } as const
/**
* Prefix constants for special tag types.
*/
const TAG_PREFIXES = { const TAG_PREFIXES = {
VARIABLE: 'variable.', VARIABLE: 'variable.',
} as const } as const
@@ -128,14 +150,29 @@ const ensureRootTag = (tags: string[], rootTag: string): string[] => {
} }
/** /**
* Gets a subblock value from the store * Gets a subblock value from the store.
*
* @param blockId - The block identifier
* @param property - The property name to retrieve
* @returns The value from the subblock store
*/ */
const getSubBlockValue = (blockId: string, property: string): any => { const getSubBlockValue = (blockId: string, property: string): any => {
return useSubBlockStore.getState().getValue(blockId, property) return useSubBlockStore.getState().getValue(blockId, property)
} }
/** /**
* Gets the output type for a specific path in a block's outputs * Gets the output type for a specific path in a block's outputs.
*
* @remarks
* Handles special cases for trigger blocks, starter blocks with chat mode,
* and tool-based operations.
*
* @param block - The block state
* @param blockConfig - The block configuration, or null
* @param blockId - The block identifier
* @param outputPath - The dot-separated path to the output field
* @param mergedSubBlocksOverride - Optional override for subblock values
* @returns The type of the output field (e.g., 'string', 'array', 'any')
*/ */
const getOutputTypeForPath = ( const getOutputTypeForPath = (
block: BlockState, block: BlockState,
@@ -181,7 +218,15 @@ const getOutputTypeForPath = (
} }
/** /**
* Recursively generates all output paths from an outputs schema * Recursively generates all output paths from an outputs schema.
*
* @remarks
* Traverses nested objects and arrays to build dot-separated paths
* for all leaf values in the schema.
*
* @param outputs - The outputs schema object
* @param prefix - Current path prefix for recursion
* @returns Array of dot-separated paths to all output fields
*/ */
const generateOutputPaths = (outputs: Record<string, any>, prefix = ''): string[] => { const generateOutputPaths = (outputs: Record<string, any>, prefix = ''): string[] => {
const paths: string[] = [] const paths: string[] = []
@@ -230,7 +275,15 @@ const generateOutputPaths = (outputs: Record<string, any>, prefix = ''): string[
} }
/** /**
* Recursively generates all output paths with their types from an outputs schema * Recursively generates all output paths with their types from an outputs schema.
*
* @remarks
* Similar to generateOutputPaths but also captures the type information
* for each path, useful for displaying type hints in the UI.
*
* @param outputs - The outputs schema object
* @param prefix - Current path prefix for recursion
* @returns Array of objects containing path and type for each output field
*/ */
const generateOutputPathsWithTypes = ( const generateOutputPathsWithTypes = (
outputs: Record<string, any>, outputs: Record<string, any>,
@@ -269,7 +322,11 @@ const generateOutputPathsWithTypes = (
} }
/** /**
* Generates output paths for a tool-based block * Generates output paths for a tool-based block.
*
* @param blockConfig - The block configuration containing tools config
* @param operation - The selected operation for the tool
* @returns Array of output paths for the tool, or empty array on error
*/ */
const generateToolOutputPaths = (blockConfig: BlockConfig, operation: string): string[] => { const generateToolOutputPaths = (blockConfig: BlockConfig, operation: string): string[] => {
if (!blockConfig?.tools?.config?.tool) return [] if (!blockConfig?.tools?.config?.tool) return []
@@ -289,7 +346,12 @@ const generateToolOutputPaths = (blockConfig: BlockConfig, operation: string): s
} }
/** /**
* Gets the output type for a specific path in a tool's outputs * Gets the output type for a specific path in a tool's outputs.
*
* @param blockConfig - The block configuration containing tools config
* @param operation - The selected operation for the tool
* @param path - The dot-separated path to the output field
* @returns The type of the output field, or 'any' if not found
*/ */
const getToolOutputType = (blockConfig: BlockConfig, operation: string, path: string): string => { const getToolOutputType = (blockConfig: BlockConfig, operation: string, path: string): string => {
if (!blockConfig?.tools?.config?.tool) return 'any' if (!blockConfig?.tools?.config?.tool) return 'any'
@@ -311,7 +373,16 @@ const getToolOutputType = (blockConfig: BlockConfig, operation: string, path: st
} }
/** /**
* Calculates the viewport position of the caret in a textarea/input * Calculates the viewport position of the caret in a textarea/input.
*
* @remarks
* Creates a hidden mirror div with identical styling to measure the
* precise position of the caret for popover anchoring.
*
* @param element - The textarea or input element
* @param caretPosition - The character position of the caret
* @param text - The text content of the element
* @returns Object with `left` and `top` viewport coordinates
*/ */
const getCaretViewportPosition = ( const getCaretViewportPosition = (
element: HTMLTextAreaElement | HTMLInputElement, element: HTMLTextAreaElement | HTMLInputElement,
@@ -361,7 +432,15 @@ const getCaretViewportPosition = (
} }
/** /**
* Renders a tag icon with background color - can use either a React icon component or a letter * Renders a tag icon with background color.
*
* @remarks
* Supports either a React icon component or a single letter string
* for flexible icon display in the tag dropdown.
*
* @param icon - Either a letter string or a Lucide icon component
* @param color - Background color for the icon container
* @returns A styled icon element
*/ */
const TagIcon: React.FC<{ const TagIcon: React.FC<{
icon: string | React.ComponentType<{ className?: string }> icon: string | React.ComponentType<{ className?: string }>
@@ -372,18 +451,29 @@ const TagIcon: React.FC<{
style={{ background: color }} style={{ background: color }}
> >
{typeof icon === 'string' ? ( {typeof icon === 'string' ? (
<span className='font-bold text-[10px] text-white'>{icon}</span> <span className='!text-white font-bold text-[10px]'>{icon}</span>
) : ( ) : (
(() => { (() => {
const IconComponent = icon const IconComponent = icon
return <IconComponent className='h-[9px] w-[9px] text-white' /> return <IconComponent className='!text-white size-[9px]' />
})() })()
)} )}
</div> </div>
) )
/** /**
* Wrapper for PopoverBackButton that handles parent tag navigation * Wrapper for PopoverBackButton that handles parent tag navigation.
*
* @remarks
* Extends the base PopoverBackButton to support selecting the parent tag
* when navigating back from a nested folder view.
*
* @param selectedIndex - Currently selected item index
* @param setSelectedIndex - Callback to update selection
* @param flatTagList - Flat list of all available tags
* @param nestedBlockTagGroups - Groups of nested block tags
* @param itemRefs - Refs to item DOM elements for scrolling
* @returns The back button component with parent tag support
*/ */
const TagDropdownBackButton: React.FC<{ const TagDropdownBackButton: React.FC<{
selectedIndex: number selectedIndex: number
@@ -432,8 +522,26 @@ const TagDropdownBackButton: React.FC<{
} }
/** /**
* TagDropdown component that displays available tags (variables and block outputs) * TagDropdown component that displays available tags for selection in input fields.
* for selection in input fields. Uses the Popover component system for consistent styling. *
* @remarks
* Displays variables and block outputs that can be referenced in workflow inputs.
* Uses the Popover component system for consistent styling and positioning.
* Supports keyboard navigation, search filtering, and nested folder views.
*
* @example
* ```tsx
* <TagDropdown
* visible={showDropdown}
* onSelect={handleTagSelect}
* blockId={currentBlockId}
* activeSourceBlockId={null}
* inputValue={inputText}
* cursorPosition={cursor}
* onClose={() => setShowDropdown(false)}
* inputRef={textareaRef}
* />
* ```
*/ */
export const TagDropdown: React.FC<TagDropdownProps> = ({ export const TagDropdown: React.FC<TagDropdownProps> = ({
visible, visible,
@@ -1382,13 +1490,13 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}} }}
> >
<TagIcon icon='V' color={BLOCK_COLORS.VARIABLE} /> <TagIcon icon='V' color={BLOCK_COLORS.VARIABLE} />
<span className='flex-1 truncate'> <span className='flex-1 truncate text-[var(--text-primary)]'>
{tag.startsWith(TAG_PREFIXES.VARIABLE) {tag.startsWith(TAG_PREFIXES.VARIABLE)
? tag.substring(TAG_PREFIXES.VARIABLE.length) ? tag.substring(TAG_PREFIXES.VARIABLE.length)
: tag} : tag}
</span> </span>
{variableInfo && ( {variableInfo && (
<span className='ml-auto text-[10px] text-[var(--white)]/60'> <span className='ml-auto text-[10px] text-[var(--text-secondary)]'>
{variableInfo.type} {variableInfo.type}
</span> </span>
)} )}
@@ -1502,9 +1610,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}} }}
> >
<TagIcon icon={tagIcon} color={blockColor} /> <TagIcon icon={tagIcon} color={blockColor} />
<span className='flex-1 truncate'>{child.display}</span> <span className='flex-1 truncate text-[var(--text-primary)]'>
{child.display}
</span>
{childType && childType !== 'any' && ( {childType && childType !== 'any' && (
<span className='ml-auto text-[10px] text-[var(--white)]/60'> <span className='ml-auto text-[10px] text-[var(--text-secondary)]'>
{childType} {childType}
</span> </span>
)} )}
@@ -1578,9 +1688,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}} }}
> >
<TagIcon icon={displayIcon} color={blockColor} /> <TagIcon icon={displayIcon} color={blockColor} />
<span className='flex-1 truncate'>{nestedTag.display}</span> <span className='flex-1 truncate text-[var(--text-primary)]'>
{nestedTag.display}
</span>
{tagDescription && tagDescription !== 'any' && ( {tagDescription && tagDescription !== 'any' && (
<span className='ml-auto text-[10px] text-[var(--white)]/60'> <span className='ml-auto text-[10px] text-[var(--text-secondary)]'>
{tagDescription} {tagDescription}
</span> </span>
)} )}

View File

@@ -1131,11 +1131,7 @@ try {
{activeSection === 'schema' && ( {activeSection === 'schema' && (
<ModalFooter className='items-center justify-between'> <ModalFooter className='items-center justify-between'>
{isEditing ? ( {isEditing ? (
<Button <Button variant='destructive' onClick={() => setShowDeleteConfirm(true)}>
variant='default'
onClick={() => setShowDeleteConfirm(true)}
className='bg-[var(--text-error)] text-white hover:bg-[var(--text-error)]'
>
Delete Delete
</Button> </Button>
) : ( ) : (
@@ -1146,7 +1142,7 @@ try {
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='tertiary'
onClick={() => setActiveSection('code')} onClick={() => setActiveSection('code')}
disabled={!isSchemaValid || !!schemaError} disabled={!isSchemaValid || !!schemaError}
> >
@@ -1159,11 +1155,7 @@ try {
{activeSection === 'code' && ( {activeSection === 'code' && (
<ModalFooter className='items-center justify-between'> <ModalFooter className='items-center justify-between'>
{isEditing ? ( {isEditing ? (
<Button <Button variant='destructive' onClick={() => setShowDeleteConfirm(true)}>
variant='default'
onClick={() => setShowDeleteConfirm(true)}
className='bg-[var(--text-error)] text-white hover:bg-[var(--text-error)]'
>
Delete Delete
</Button> </Button>
) : ( ) : (
@@ -1176,7 +1168,7 @@ try {
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='tertiary'
onClick={handleSave} onClick={handleSave}
disabled={!isSchemaValid || !!schemaError} disabled={!isSchemaValid || !!schemaError}
> >
@@ -1192,7 +1184,7 @@ try {
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Custom Tool</ModalHeader> <ModalHeader>Delete Custom Tool</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
This will permanently delete the tool and remove it from any workflows that are using This will permanently delete the tool and remove it from any workflows that are using
it. <span className='text-[var(--text-error)]'>This action cannot be undone.</span> it. <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p> </p>
@@ -1206,10 +1198,9 @@ try {
Cancel Cancel
</Button> </Button>
<Button <Button
variant='primary' variant='destructive'
onClick={handleDelete} onClick={handleDelete}
disabled={deleteToolMutation.isPending} disabled={deleteToolMutation.isPending}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
> >
{deleteToolMutation.isPending ? 'Deleting...' : 'Delete'} {deleteToolMutation.isPending ? 'Deleting...' : 'Delete'}
</Button> </Button>

View File

@@ -98,10 +98,10 @@ export function McpToolsList({
}} }}
> >
<div <div
className='flex h-[15px] w-[15px] flex-shrink-0 items-center justify-center rounded' className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: mcpTool.bgColor }} style={{ background: mcpTool.bgColor }}
> >
<IconComponent icon={mcpTool.icon} className='h-[11px] w-[11px] text-white' /> <IconComponent icon={mcpTool.icon} className='h-[9px] w-[9px] text-white' />
</div> </div>
<span className='truncate' title={`${mcpTool.name} (${mcpTool.serverName})`}> <span className='truncate' title={`${mcpTool.name} (${mcpTool.serverName})`}>
{mcpTool.name} {mcpTool.name}

View File

@@ -232,8 +232,8 @@ export function CommandItem({
<button <button
id={value} id={value}
className={cn( className={cn(
'flex h-[25px] w-full cursor-pointer select-none items-center gap-[8px] rounded-[6px] px-[6px] font-base text-[12px] text-[var(--text-primary)] outline-none transition-colors hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:pointer-events-none data-[selected=true]:bg-[var(--surface-9)] data-[selected=true]:text-[var(--text-primary)] data-[disabled=true]:opacity-50', 'flex h-[25px] w-full cursor-pointer select-none items-center gap-[8px] rounded-[6px] px-[6px] font-base text-[12px] text-[var(--text-primary)] outline-none transition-colors hover:bg-[var(--surface-5)] hover:text-[var(--text-primary)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:pointer-events-none data-[selected=true]:bg-[var(--surface-5)] data-[selected=true]:text-[var(--text-primary)] data-[disabled=true]:opacity-50',
(isActive || isHovered) && 'bg-[var(--surface-9)] text-[var(--text-primary)]', (isActive || isHovered) && 'bg-[var(--surface-5)] text-[var(--text-primary)]',
className className
)} )}
onClick={() => !disabled && onSelect?.()} onClick={() => !disabled && onSelect?.()}

View File

@@ -379,11 +379,7 @@ export function TriggerSave({
disabled={disabled || isProcessing} disabled={disabled || isProcessing}
className='h-[32px] rounded-[8px] px-[12px]' className='h-[32px] rounded-[8px] px-[12px]'
> >
{deleteStatus === 'deleting' ? ( <Trash className='h-[14px] w-[14px]' />
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
) : (
<Trash className='h-[14px] w-[14px]' />
)}
</Button> </Button>
)} )}
</div> </div>
@@ -439,7 +435,7 @@ export function TriggerSave({
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Trigger</ModalHeader> <ModalHeader>Delete Trigger</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete this trigger configuration? This will remove the Are you sure you want to delete this trigger configuration? This will remove the
webhook and stop all incoming triggers.{' '} webhook and stop all incoming triggers.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span> <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
@@ -449,11 +445,7 @@ export function TriggerSave({
<Button variant='active' onClick={() => setShowDeleteDialog(false)}> <Button variant='active' onClick={() => setShowDeleteDialog(false)}>
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={handleDeleteConfirm}>
variant='primary'
onClick={handleDeleteConfirm}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
Delete Delete
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -294,7 +294,7 @@ export function VariablesInput({
key={assignment.id} key={assignment.id}
data-assignment-id={assignment.id} data-assignment-id={assignment.id}
className={cn( className={cn(
'rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]', 'rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]',
collapsed ? 'overflow-hidden' : 'overflow-visible' collapsed ? 'overflow-hidden' : 'overflow-visible'
)} )}
> >
@@ -336,7 +336,7 @@ export function VariablesInput({
</div> </div>
{!collapsed && ( {!collapsed && (
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] border-t px-[10px] pt-[6px] pb-[10px]'> <div className='flex flex-col gap-[6px] border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[4px]'> <div className='flex flex-col gap-[4px]'>
<Label className='text-[13px]'>Variable</Label> <Label className='text-[13px]'>Variable</Label>
<Combobox <Combobox

View File

@@ -54,7 +54,11 @@ export interface WandControlHandlers {
} }
/** /**
* Props for the `SubBlock` UI element. Renders a single configurable input within a workflow block. * Props for the SubBlock component.
*
* @remarks
* SubBlock renders a single configurable input within a workflow block,
* supporting various input types, preview mode, and conditional requirements.
*/ */
interface SubBlockProps { interface SubBlockProps {
blockId: string blockId: string
@@ -68,10 +72,14 @@ interface SubBlockProps {
/** /**
* Returns whether the field is required for validation. * Returns whether the field is required for validation.
*
* @remarks
* Evaluates conditional requirements based on current field values. * Evaluates conditional requirements based on current field values.
* @param config - The sub-block configuration * Supports boolean, condition objects, and functions that return conditions.
* @param subBlockValues - Current values of all subblocks *
* @returns True if the field is required * @param config - The sub-block configuration containing requirement rules
* @param subBlockValues - Current values of all subblocks for condition evaluation
* @returns `true` if the field is required based on current context
*/ */
const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string, any>): boolean => { const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string, any>): boolean => {
if (!config.required) return false if (!config.required) return false
@@ -126,10 +134,15 @@ const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string,
/** /**
* Retrieves the preview value for a specific sub-block. * Retrieves the preview value for a specific sub-block.
*
* @remarks
* Only returns a value when in preview mode and subBlockValues are provided.
* Returns `null` if the value is not found in the subblock values.
*
* @param config - The sub-block configuration * @param config - The sub-block configuration
* @param isPreview - Whether the component is in preview mode * @param isPreview - Whether the component is in preview mode
* @param subBlockValues - Optional record of sub-block values * @param subBlockValues - Optional record of sub-block values
* @returns The preview value or undefined * @returns The preview value, `null` if not found, or `undefined` if not in preview
*/ */
const getPreviewValue = ( const getPreviewValue = (
config: SubBlockConfig, config: SubBlockConfig,
@@ -142,11 +155,16 @@ const getPreviewValue = (
/** /**
* Renders the label with optional validation, description tooltips, and inline wand control. * Renders the label with optional validation, description tooltips, and inline wand control.
* @param config - The sub-block configuration *
* @param isValidJson - Whether the JSON is valid * @remarks
* @param wandState - Wand interaction state * Handles JSON validation indicators for code blocks, required field markers,
* and AI generation (wand) input interface.
*
* @param config - The sub-block configuration defining the label content
* @param isValidJson - Whether the JSON content is valid (for code blocks)
* @param wandState - State and handlers for the AI wand feature
* @param subBlockValues - Current values of all subblocks for evaluating conditional requirements * @param subBlockValues - Current values of all subblocks for evaluating conditional requirements
* @returns The label JSX element or null if no title or for switch types * @returns The label JSX element, or `null` for switch types or when no title is defined
*/ */
const renderLabel = ( const renderLabel = (
config: SubBlockConfig, config: SubBlockConfig,
@@ -251,9 +269,14 @@ const renderLabel = (
/** /**
* Compares props to prevent unnecessary re-renders. * Compares props to prevent unnecessary re-renders.
*
* @remarks
* Used with React.memo to optimize performance by skipping re-renders
* when props haven't meaningfully changed.
*
* @param prevProps - Previous component props * @param prevProps - Previous component props
* @param nextProps - Next component props * @param nextProps - Next component props
* @returns True if props are equal (skip re-render) * @returns `true` if props are equal and re-render should be skipped
*/ */
const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): boolean => { const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): boolean => {
return ( return (
@@ -268,7 +291,21 @@ const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): bool
} }
/** /**
* Renders a single workflow sub-block input based on `config.type`, supporting preview and disabled states. * Renders a single workflow sub-block input based on config.type.
*
* @remarks
* Supports multiple input types including short-input, long-input, dropdown,
* combobox, slider, table, code, switch, tool-input, and many more.
* Handles preview mode, disabled states, and AI wand generation.
*
* @param blockId - The parent block identifier
* @param config - Configuration defining the input type and properties
* @param isPreview - Whether to render in preview mode
* @param subBlockValues - Current values of all subblocks
* @param disabled - Whether the input is disabled
* @param fieldDiffStatus - Optional diff status for visual indicators
* @param allowExpandInPreview - Whether to allow expanding in preview mode
* @returns The rendered sub-block input component
*/ */
function SubBlockComponent({ function SubBlockComponent({
blockId, blockId,
@@ -297,7 +334,8 @@ function SubBlockComponent({
const isWandEnabled = config.wandConfig?.enabled ?? false const isWandEnabled = config.wandConfig?.enabled ?? false
/** /**
* Handle wand icon click to activate inline prompt mode * Handles wand icon click to activate inline prompt mode.
* Focuses the input after a brief delay to ensure DOM is ready.
*/ */
const handleSearchClick = (): void => { const handleSearchClick = (): void => {
setIsSearchActive(true) setIsSearchActive(true)
@@ -307,7 +345,8 @@ function SubBlockComponent({
} }
/** /**
* Handle search input blur - deactivate if empty and not streaming * Handles search input blur event.
* Deactivates the search mode if the query is empty and not currently streaming.
*/ */
const handleSearchBlur = (): void => { const handleSearchBlur = (): void => {
if (!searchQuery.trim() && !wandControlRef.current?.isWandStreaming) { if (!searchQuery.trim() && !wandControlRef.current?.isWandStreaming) {
@@ -316,14 +355,17 @@ function SubBlockComponent({
} }
/** /**
* Handle search query change * Handles search query change.
*
* @param value - The new search query value
*/ */
const handleSearchChange = (value: string): void => { const handleSearchChange = (value: string): void => {
setSearchQuery(value) setSearchQuery(value)
} }
/** /**
* Handle search submit - trigger generation * Handles search submit to trigger AI generation.
* Clears the query and deactivates search mode after submission.
*/ */
const handleSearchSubmit = (): void => { const handleSearchSubmit = (): void => {
if (searchQuery.trim() && wandControlRef.current) { if (searchQuery.trim() && wandControlRef.current) {
@@ -334,7 +376,8 @@ function SubBlockComponent({
} }
/** /**
* Handle search cancel * Handles search cancel to exit AI prompt mode.
* Clears the query and deactivates search mode.
*/ */
const handleSearchCancel = (): void => { const handleSearchCancel = (): void => {
setSearchQuery('') setSearchQuery('')
@@ -358,7 +401,13 @@ function SubBlockComponent({
const isDisabled = gatedDisabled const isDisabled = gatedDisabled
/** /**
* Selects and renders the appropriate input component for the current sub-block `config.type`. * Selects and renders the appropriate input component based on config.type.
*
* @remarks
* Maps the config type to the corresponding input component with all
* necessary props. Falls back to an error message for unknown types.
*
* @returns The appropriate input component JSX element
*/ */
const renderInput = (): JSX.Element => { const renderInput = (): JSX.Element => {
switch (config.type) { switch (config.type) {

View File

@@ -172,9 +172,9 @@ export function Editor() {
return ( return (
<div className='flex h-full flex-col'> <div className='flex h-full flex-col'>
{/* Header */} {/* Header */}
<div className='flex flex-shrink-0 items-center justify-between rounded-[4px] bg-[var(--surface-5)] px-[12px] py-[8px]'> <div className='mx-[-1px] flex flex-shrink-0 items-center justify-between rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] px-[12px] py-[6px]'>
<div className='flex min-w-0 flex-1 items-center gap-[8px]'> <div className='flex min-w-0 flex-1 items-center gap-[8px]'>
{(blockConfig || isSubflow) && ( {(blockConfig || isSubflow) && currentBlock?.type !== 'note' && (
<div <div
className='flex h-[18px] w-[18px] items-center justify-center rounded-[4px]' className='flex h-[18px] w-[18px] items-center justify-center rounded-[4px]'
style={{ background: isSubflow ? subflowBgColor : blockConfig?.bgColor }} style={{ background: isSubflow ? subflowBgColor : blockConfig?.bgColor }}

View File

@@ -488,7 +488,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
> >
{/* Header */} {/* Header */}
<div <div
className='flex flex-shrink-0 cursor-pointer items-center justify-between rounded-[4px] bg-[var(--surface-5)] px-[12px] py-[8px]' className='mx-[-1px] flex flex-shrink-0 cursor-pointer items-center justify-between rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] px-[12px] py-[6px]'
onClick={handleSearchClick} onClick={handleSearchClick}
> >
<h2 className='font-medium text-[14px] text-[var(--text-primary)]'>Toolbar</h2> <h2 className='font-medium text-[14px] text-[var(--text-primary)]'>Toolbar</h2>
@@ -532,7 +532,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
Triggers Triggers
</div> </div>
<div className='flex-1 overflow-y-auto overflow-x-hidden px-[6px]'> <div className='flex-1 overflow-y-auto overflow-x-hidden px-[6px]'>
<div ref={triggersContentRef} className='space-y-[4px] pb-[8px]'> <div ref={triggersContentRef} className='space-y-[2px] pb-[8px]'>
{filteredTriggers.map((trigger, index) => { {filteredTriggers.map((trigger, index) => {
const Icon = trigger.icon const Icon = trigger.icon
const isTriggerCapable = hasTriggerCapability(trigger) const isTriggerCapable = hasTriggerCapability(trigger)
@@ -554,9 +554,9 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
}} }}
onClick={() => handleItemClick(trigger.type, isTriggerCapable)} onClick={() => handleItemClick(trigger.type, isTriggerCapable)}
className={clsx( className={clsx(
'group flex h-[25px] items-center gap-[8px] rounded-[8px] px-[5.5px] text-[14px]', 'group flex h-[28px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]',
'cursor-pointer hover:bg-[var(--surface-9)] active:cursor-grabbing', 'cursor-pointer hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
'focus-visible:bg-[var(--surface-9)] focus-visible:outline-none' 'focus-visible:bg-[var(--surface-6)] focus-visible:outline-none dark:focus-visible:bg-[var(--surface-5)]'
)} )}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
@@ -567,7 +567,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
}} }}
> >
<div <div
className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]' className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ background: trigger.bgColor }} style={{ background: trigger.bgColor }}
> >
{Icon && ( {Icon && (
@@ -575,7 +575,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
className={clsx( className={clsx(
'toolbar-item-icon text-white transition-transform duration-200', 'toolbar-item-icon text-white transition-transform duration-200',
'group-hover:scale-110', 'group-hover:scale-110',
'!h-[9px] !w-[9px]' '!h-[10px] !w-[10px]'
)} )}
/> />
)} )}
@@ -614,7 +614,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
Blocks Blocks
</div> </div>
<div className='flex-1 overflow-y-auto overflow-x-hidden px-[6px]'> <div className='flex-1 overflow-y-auto overflow-x-hidden px-[6px]'>
<div className='space-y-[4px] pb-[8px]'> <div className='space-y-[2px] pb-[8px]'>
{filteredBlocks.map((block, index) => { {filteredBlocks.map((block, index) => {
const Icon = block.icon const Icon = block.icon
return ( return (
@@ -643,9 +643,9 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
}} }}
onClick={() => handleItemClick(block.type, false)} onClick={() => handleItemClick(block.type, false)}
className={clsx( className={clsx(
'group flex h-[25px] items-center gap-[8px] rounded-[8px] px-[5.5px] text-[14px]', 'group flex h-[28px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]',
'cursor-pointer hover:bg-[var(--surface-9)] active:cursor-grabbing', 'cursor-pointer hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
'focus-visible:bg-[var(--surface-9)] focus-visible:outline-none' 'focus-visible:bg-[var(--surface-6)] focus-visible:outline-none dark:focus-visible:bg-[var(--surface-5)]'
)} )}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
@@ -656,7 +656,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
}} }}
> >
<div <div
className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]' className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ background: block.bgColor }} style={{ background: block.bgColor }}
> >
{Icon && ( {Icon && (
@@ -664,7 +664,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
className={clsx( className={clsx(
'toolbar-item-icon text-white transition-transform duration-200', 'toolbar-item-icon text-white transition-transform duration-200',
'group-hover:scale-110', 'group-hover:scale-110',
'!h-[9px] !w-[9px]' '!h-[10px] !w-[10px]'
)} )}
/> />
)} )}

View File

@@ -1,10 +1,5 @@
/**
* @deprecated This component is deprecated and kept as reference only.
*/
'use client' 'use client'
import { useStore } from 'reactflow'
import { Button, Redo, Undo } from '@/components/emcn' import { Button, Redo, Undo } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client' import { useSession } from '@/lib/auth/auth-client'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
@@ -12,15 +7,10 @@ import { useUndoRedoStore } from '@/stores/undo-redo'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
/** /**
* Workflow controls component that provides undo/redo and zoom functionality * Workflow controls component that provides undo/redo functionality.
* Integrates directly into the panel header for easy access * Styled to align with the panel tab buttons.
*/ */
export function WorkflowControls() { export function WorkflowControls() {
// Subscribe to React Flow store so zoom % live-updates while zooming
const zoom = useStore((s: any) =>
Array.isArray(s.transform) ? s.transform[2] : s.viewport?.zoom
)
const { undo, redo } = useCollaborativeWorkflow() const { undo, redo } = useCollaborativeWorkflow()
const { activeWorkflowId } = useWorkflowRegistry() const { activeWorkflowId } = useWorkflowRegistry()
const { data: session } = useSession() const { data: session } = useSession()
@@ -33,34 +23,28 @@ export function WorkflowControls() {
return { undoSize: stack.undo.length, redoSize: stack.redo.length } return { undoSize: stack.undo.length, redoSize: stack.redo.length }
})() })()
const currentZoom = Math.round(((zoom as number) || 1) * 100) const canUndo = undoRedoSizes.undoSize > 0
const canRedo = undoRedoSizes.redoSize > 0
return ( return (
<div className='flex items-center gap-[4px]'> <div className='flex gap-[2px]'>
{/* Undo/Redo Controls - Connected Two-Sided Button */} <Button
<div className='flex gap-[1px]'> className='h-[28px] rounded-[6px] rounded-r-none border border-transparent px-[6px] py-[5px] hover:border-[var(--border-1)] hover:bg-[var(--surface-5)]'
<Button onClick={undo}
className='h-[28px] rounded-r-none px-[8px] py-[5px] text-[12.5px]' variant={canUndo ? 'active' : 'ghost'}
onClick={undo} disabled={!canUndo}
variant={undoRedoSizes.undoSize === 0 ? 'default' : 'active'} title='Undo (Cmd+Z)'
title='Undo (Cmd+Z)' >
> <Undo className='h-[12px] w-[12px]' />
<Undo className='h-[12px] w-[12px]' /> </Button>
</Button> <Button
className='h-[28px] rounded-[6px] rounded-l-none border border-transparent px-[6px] py-[5px] hover:border-[var(--border-1)] hover:bg-[var(--surface-5)]'
<Button onClick={redo}
className='h-[28px] rounded-l-none px-[8px] py-[5px] text-[12.5px]' variant={canRedo ? 'active' : 'ghost'}
onClick={redo} disabled={!canRedo}
variant={undoRedoSizes.redoSize === 0 ? 'default' : 'active'} title='Redo (Cmd+Shift+Z)'
title='Redo (Cmd+Shift+Z)' >
> <Redo className='h-[12px] w-[12px]' />
<Redo className='h-[12px] w-[12px]' />
</Button>
</div>
{/* Zoom Badge */}
<Button className='flex h-[28px] w-[40px] items-center justify-center rounded-[4px] px-[8px] py-[5px] font-medium text-[12.5px]'>
{currentZoom}%
</Button> </Button>
</div> </div>
) )

View File

@@ -352,10 +352,10 @@ export function Panel() {
{/* Header */} {/* Header */}
<div className='flex flex-shrink-0 items-center justify-between px-[8px]'> <div className='flex flex-shrink-0 items-center justify-between px-[8px]'>
{/* More and Chat */} {/* More and Chat */}
<div className='flex gap-[4px]'> <div className='flex gap-[6px]'>
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}> <Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button className='h-[32px] w-[32px]'> <Button className='h-[30px] w-[30px] rounded-[5px]'>
<MoreHorizontal /> <MoreHorizontal />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -408,7 +408,7 @@ export function Panel() {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<Button <Button
className='h-[32px] w-[32px]' className='h-[30px] w-[30px] rounded-[5px]'
variant={isChatOpen ? 'active' : 'default'} variant={isChatOpen ? 'active' : 'default'}
onClick={() => setIsChatOpen(!isChatOpen)} onClick={() => setIsChatOpen(!isChatOpen)}
> >
@@ -417,11 +417,11 @@ export function Panel() {
</div> </div>
{/* Deploy and Run */} {/* Deploy and Run */}
<div className='flex gap-[4px]'> <div className='flex gap-[6px]'>
<Deploy activeWorkflowId={activeWorkflowId} userPermissions={userPermissions} /> <Deploy activeWorkflowId={activeWorkflowId} userPermissions={userPermissions} />
<Button <Button
className='h-[32px] w-[61.5px] gap-[8px]' className='h-[30px] gap-[8px] px-[10px]'
variant={isExecuting ? 'active' : 'primary'} variant={isExecuting ? 'active' : 'tertiary'}
onClick={isExecuting ? cancelWorkflow : () => runWorkflow()} onClick={isExecuting ? cancelWorkflow : () => runWorkflow()}
disabled={!isExecuting && isButtonDisabled} disabled={!isExecuting && isButtonDisabled}
> >
@@ -430,7 +430,7 @@ export function Panel() {
) : ( ) : (
<Play className='h-[11.5px] w-[11.5px]' /> <Play className='h-[11.5px] w-[11.5px]' />
)} )}
Run {isExecuting ? 'Stop' : 'Run'}
</Button> </Button>
</div> </div>
</div> </div>
@@ -439,7 +439,11 @@ export function Panel() {
<div className='flex flex-shrink-0 items-center justify-between px-[8px] pt-[14px]'> <div className='flex flex-shrink-0 items-center justify-between px-[8px] pt-[14px]'>
<div className='flex gap-[4px]'> <div className='flex gap-[4px]'>
<Button <Button
className='h-[28px] truncate px-[8px] py-[5px] text-[12.5px] hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)]' className={`h-[28px] truncate rounded-[6px] border px-[8px] py-[5px] text-[12.5px] ${
_hasHydrated && activeTab === 'copilot'
? 'border-[var(--border-1)]'
: 'border-transparent hover:border-[var(--border-1)] hover:bg-[var(--surface-5)] hover:text-[var(--text-primary)]'
}`}
variant={_hasHydrated && activeTab === 'copilot' ? 'active' : 'ghost'} variant={_hasHydrated && activeTab === 'copilot' ? 'active' : 'ghost'}
onClick={() => handleTabClick('copilot')} onClick={() => handleTabClick('copilot')}
data-tab-button='copilot' data-tab-button='copilot'
@@ -447,7 +451,11 @@ export function Panel() {
Copilot Copilot
</Button> </Button>
<Button <Button
className='h-[28px] px-[8px] py-[5px] text-[12.5px] hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)]' className={`h-[28px] rounded-[6px] border px-[8px] py-[5px] text-[12.5px] ${
_hasHydrated && activeTab === 'toolbar'
? 'border-[var(--border-1)]'
: 'border-transparent hover:border-[var(--border-1)] hover:bg-[var(--surface-5)] hover:text-[var(--text-primary)]'
}`}
variant={_hasHydrated && activeTab === 'toolbar' ? 'active' : 'ghost'} variant={_hasHydrated && activeTab === 'toolbar' ? 'active' : 'ghost'}
onClick={() => handleTabClick('toolbar')} onClick={() => handleTabClick('toolbar')}
data-tab-button='toolbar' data-tab-button='toolbar'
@@ -455,7 +463,11 @@ export function Panel() {
Toolbar Toolbar
</Button> </Button>
<Button <Button
className='h-[28px] px-[8px] py-[5px] text-[12.5px] hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)]' className={`h-[28px] rounded-[6px] border px-[8px] py-[5px] text-[12.5px] ${
_hasHydrated && activeTab === 'editor'
? 'border-[var(--border-1)]'
: 'border-transparent hover:border-[var(--border-1)] hover:bg-[var(--surface-5)] hover:text-[var(--text-primary)]'
}`}
variant={_hasHydrated && activeTab === 'editor' ? 'active' : 'ghost'} variant={_hasHydrated && activeTab === 'editor' ? 'active' : 'ghost'}
onClick={() => handleTabClick('editor')} onClick={() => handleTabClick('editor')}
data-tab-button='editor' data-tab-button='editor'
@@ -464,7 +476,7 @@ export function Panel() {
</Button> </Button>
</div> </div>
{/* Workflow Controls (Undo/Redo and Zoom) */} {/* Workflow Controls (Undo/Redo) */}
{/* <WorkflowControls /> */} {/* <WorkflowControls /> */}
</div> </div>
@@ -524,7 +536,7 @@ export function Panel() {
<ModalContent size='sm'> <ModalContent size='sm'>
<ModalHeader>Delete Workflow</ModalHeader> <ModalHeader>Delete Workflow</ModalHeader>
<ModalBody> <ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'> <p className='text-[12px] text-[var(--text-secondary)]'>
Deleting this workflow will permanently remove all associated blocks, executions, and Deleting this workflow will permanently remove all associated blocks, executions, and
configuration.{' '} configuration.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span> <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
@@ -538,12 +550,7 @@ export function Panel() {
> >
Cancel Cancel
</Button> </Button>
<Button <Button variant='destructive' onClick={handleDeleteWorkflow} disabled={isDeleting}>
variant='primary'
onClick={handleDeleteWorkflow}
disabled={isDeleting}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{isDeleting ? 'Deleting...' : 'Delete'} {isDeleting ? 'Deleting...' : 'Delete'}
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -1,183 +0,0 @@
'use client'
import { Bug, Copy, Layers, Play, Rocket, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
const SkeletonControlBar = () => {
return (
<div className='fixed top-4 right-4 z-20 flex items-center gap-1'>
{/* Delete Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))]'
disabled
>
<Trash2 className='h-5 w-5' />
</Button>
{/* Duplicate Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))] hover:bg-gray-100'
disabled
>
<Copy className='h-5 w-5' />
</Button>
{/* Auto Layout Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))] hover:bg-gray-100'
disabled
>
<Layers className='h-5 w-5' />
</Button>
{/* Debug Mode Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))] hover:bg-gray-100'
disabled
>
<Bug className='h-5 w-5' />
</Button>
{/* Deploy Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))] hover:bg-gray-100'
disabled
>
<Rocket className='h-5 w-5' />
</Button>
{/* Run Button */}
<Button
className='h-12 cursor-not-allowed gap-2 rounded-[11px] bg-[var(--brand-primary-hex)] px-4 py-2 font-medium 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:opacity-50 disabled:hover:bg-[var(--brand-primary-hex)] disabled:hover:shadow-none'
disabled
>
<Play className='h-3.5 w-3.5 fill-current stroke-current' />
</Button>
</div>
)
}
const SkeletonPanelComponent = () => {
return (
<div className='fixed top-0 right-0 z-10'>
{/* Panel skeleton */}
<div className='h-96 w-80 space-y-4 rounded-bl-lg border-b border-l bg-background p-4'>
{/* Tab headers skeleton */}
<div className='flex gap-2 border-b pb-2'>
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className='h-6 w-16' />
))}
</div>
{/* Content skeleton */}
<div className='space-y-3'>
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className='h-4' style={{ width: `${Math.random() * 40 + 60}%` }} />
))}
</div>
</div>
</div>
)
}
const SkeletonNodes = () => {
return [
// Starter node skeleton
{
id: 'skeleton-starter',
type: 'workflowBlock',
position: { x: 100, y: 100 },
data: {
type: 'skeleton',
config: { name: '', description: '', bgColor: '#9CA3AF' },
name: '',
isActive: false,
isPending: false,
isSkeleton: true,
},
dragHandle: '.workflow-drag-handle',
},
// Additional skeleton nodes
{
id: 'skeleton-node-1',
type: 'workflowBlock',
position: { x: 500, y: 100 },
data: {
type: 'skeleton',
config: { name: '', description: '', bgColor: '#9CA3AF' },
name: '',
isActive: false,
isPending: false,
isSkeleton: true,
},
dragHandle: '.workflow-drag-handle',
},
{
id: 'skeleton-node-2',
type: 'workflowBlock',
position: { x: 300, y: 300 },
data: {
type: 'skeleton',
config: { name: '', description: '', bgColor: '#9CA3AF' },
name: '',
isActive: false,
isPending: false,
isSkeleton: true,
},
dragHandle: '.workflow-drag-handle',
},
]
}
interface SkeletonLoadingProps {
showSkeleton: boolean
isSidebarCollapsed: boolean
children: React.ReactNode
}
export function SkeletonLoading({
showSkeleton,
isSidebarCollapsed,
children,
}: SkeletonLoadingProps) {
return (
<div className='flex h-full w-full flex-1 flex-col overflow-hidden'>
{/* Skeleton Control Bar */}
<div
className={`transition-opacity duration-500 ${showSkeleton ? 'opacity-100' : 'pointer-events-none absolute opacity-0'}`}
style={{ zIndex: showSkeleton ? 10 : -1 }}
>
<SkeletonControlBar />
</div>
{/* Real Control Bar */}
<div
className={`transition-opacity duration-500 ${showSkeleton ? 'pointer-events-none absolute opacity-0' : 'opacity-100'}`}
style={{ zIndex: showSkeleton ? -1 : 10 }}
>
{children}
</div>
{/* Real content will be rendered by children - sidebar will show its own loading state */}
</div>
)
}
export function SkeletonPanelWrapper({ showSkeleton }: { showSkeleton: boolean }) {
return (
<div
className={`transition-opacity duration-500 ${showSkeleton ? 'opacity-100' : 'pointer-events-none absolute opacity-0'}`}
style={{ zIndex: showSkeleton ? 10 : -1 }}
>
<SkeletonPanelComponent />
</div>
)
}
export { SkeletonNodes, SkeletonPanelComponent }

View File

@@ -108,7 +108,7 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat
*/ */
const getHandleClasses = (position: 'left' | 'right') => { const getHandleClasses = (position: 'left' | 'right') => {
const baseClasses = '!z-[10] !cursor-crosshair !border-none !transition-[colors] !duration-150' const baseClasses = '!z-[10] !cursor-crosshair !border-none !transition-[colors] !duration-150'
const colorClasses = '!bg-[var(--surface-12)]' const colorClasses = '!bg-[var(--surface-7)]'
const positionClasses = { const positionClasses = {
left: '!left-[-7px] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none hover:!left-[-10px] hover:!w-[10px] hover:!rounded-l-full', left: '!left-[-7px] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none hover:!left-[-10px] hover:!w-[10px] hover:!rounded-l-full',

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