improvement(platform): chat, emcn, terminal, usage-limit (#1974)

* improvement(usage-indicator): layout

* improvement: expand default terminal height

* fix: swap workflow block ports

* improvement: chat initial positioning

* improvement(chat): display; improvement(emcn): popover attributes
This commit is contained in:
Emir Karabeg
2025-11-13 16:02:13 -08:00
committed by GitHub
parent b67b4ff8fb
commit 80eaeb00c2
9 changed files with 91 additions and 60 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
--panel-width: 244px;
--toolbar-triggers-height: 300px;
--editor-connections-height: 200px;
--terminal-height: 145px;
--terminal-height: 196px;
}
.sidebar-container {
@@ -569,7 +569,7 @@ export function Chat() {
return (
<div
className='fixed z-30 flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-1)] px-[10px] pt-[2px] pb-[8px]'
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]'
style={{
left: `${actualPosition.x}px`,
top: `${actualPosition.y}px`,
@@ -619,6 +619,7 @@ export function Chat() {
side='bottom'
align='end'
sideOffset={8}
maxHeight={100}
style={{ width: '110px', minWidth: '110px' }}
>
<PopoverScrollArea>
@@ -288,9 +288,11 @@ export function OutputSelect({
<PopoverContent
ref={popoverRef}
side='bottom'
align='start'
align='end'
sideOffset={4}
maxHeight={280}
maxHeight={140}
maxWidth={140}
minWidth={140}
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
>
@@ -298,26 +300,29 @@ export function OutputSelect({
{Object.entries(groupedOutputs).map(([blockName, outputs]) => (
<div key={blockName}>
<PopoverSection>{blockName}</PopoverSection>
{outputs.map((output) => (
<PopoverItem
key={output.id}
active={isSelectedValue(output)}
onClick={() => handleOutputSelection(output.label)}
>
<div
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
style={{
backgroundColor: getOutputColor(output.blockId, output.blockType),
}}
<div className='flex flex-col gap-[2px]'>
{outputs.map((output) => (
<PopoverItem
key={output.id}
active={isSelectedValue(output)}
onClick={() => handleOutputSelection(output.label)}
>
<span className='font-bold text-[10px] text-white'>
{blockName.charAt(0).toUpperCase()}
</span>
</div>
<span className='min-w-0 flex-1 truncate'>{output.path}</span>
{isSelectedValue(output) && <Check className='h-3 w-3 flex-shrink-0' />}
</PopoverItem>
))}
<div
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
style={{
backgroundColor: getOutputColor(output.blockId, output.blockType),
}}
>
<span className='font-bold text-[10px] text-white'>
{blockName.charAt(0).toUpperCase()}
</span>
</div>
<span className='min-w-0 flex-1 truncate'>{output.path}</span>
{isSelectedValue(output) && <Check className='h-3 w-3 flex-shrink-0' />}
</PopoverItem>
))}
</div>
</div>
))}
</PopoverScrollArea>
@@ -42,7 +42,7 @@ import { useOutputPanelResize, useTerminalFilters, useTerminalResize } from './h
*/
const MIN_HEIGHT = 30
const NEAR_MIN_THRESHOLD = 40
const DEFAULT_EXPANDED_HEIGHT = 300
const DEFAULT_EXPANDED_HEIGHT = 196
/**
* Column width constants - numeric values for calculations
@@ -1,6 +1,6 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'next/navigation'
import { Handle, type NodeProps, Position } from 'reactflow'
import { Handle, type NodeProps, Position, useUpdateNodeInternals } from 'reactflow'
import { Badge } from '@/components/emcn/components/badge/badge'
import { Tooltip } from '@/components/emcn/components/tooltip/tooltip'
import { getEnv, isTruthy } from '@/lib/env'
@@ -689,6 +689,16 @@ export const WorkflowBlock = memo(function WorkflowBlock({
],
})
/**
* Notify React Flow when handle orientation changes so it can recalculate edge paths.
* This is necessary because toggling handles doesn't change block dimensions,
* so useBlockDimensions won't trigger updateNodeInternals.
*/
const updateNodeInternals = useUpdateNodeInternals()
useEffect(() => {
updateNodeInternals(id)
}, [horizontalHandles, id, updateNodeInternals])
const showWebhookIndicator = (isStarterBlock || isWebhookTriggerBlock) && isWebhookConfigured
const shouldShowScheduleBadge =
type === 'schedule' && !isLoadingScheduleInfo && scheduleInfo !== null
@@ -62,11 +62,14 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
if (isLoading) {
return (
<div className='flex flex-shrink-0 flex-col gap-[10px] border-t px-[13.5px] pt-[10px] pb-[8px] dark:border-[var(--border)]'>
<div className='flex flex-shrink-0 flex-col gap-[8px] border-t pt-[12px] pr-[13.5px] pb-[10px] pl-[12px] dark:border-[var(--border)]'>
{/* Top row skeleton */}
<div className='flex items-center justify-between'>
<Skeleton className='h-[16px] w-[120px] rounded-[4px]' />
<Skeleton className='h-[16px] w-[50px] rounded-[4px]' />
<div className='flex items-center gap-[6px]'>
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
<Skeleton className='h-[14px] w-[70px] rounded-[4px]' />
</div>
<Skeleton className='h-[12px] w-[50px] rounded-[4px]' />
</div>
{/* Pills skeleton */}
@@ -121,25 +124,25 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
}
return (
<div className='flex flex-shrink-0 flex-col gap-[10px] border-t px-[13.5px] pt-[8px] pb-[8px] dark:border-[var(--border)]'>
<div className='flex flex-shrink-0 flex-col gap-[8px] border-t px-[13.5px] pt-[8px] pb-[10px] dark:border-[var(--border)]'>
{/* Top row */}
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[6px]'>
<span className='font-medium text-[#FFFFFF] text-[12px]'>{PLAN_NAMES[planType]}</span>
<div className='h-[14px] w-[1.5px] bg-[#4A4A4A]' />
<div className='h-[14px] w-[1.5px] bg-[var(--divider)]' />
<div className='flex items-center gap-[4px]'>
{isBlocked ? (
<>
<span className='font-medium text-[#B1B1B1] text-[12px]'>Over</span>
<span className='font-medium text-[#B1B1B1] text-[12px]'>limit</span>
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>Over</span>
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>limit</span>
</>
) : (
<>
<span className='font-medium text-[#B1B1B1] text-[12px] tabular-nums'>
<span className='font-medium text-[12px] text-[var(--text-tertiary)] tabular-nums'>
${usage.current.toFixed(2)}
</span>
<span className='font-medium text-[#B1B1B1] text-[12px]'>/</span>
<span className='font-medium text-[#B1B1B1] text-[12px] tabular-nums'>
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>/</span>
<span className='font-medium text-[12px] text-[var(--text-tertiary)] tabular-nums'>
${usage.limit}
</span>
</>
@@ -149,7 +152,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
{showUpgradeButton && (
<Button
variant='ghost'
className='!h-auto !px-1 !py-0 -mx-1 mt-[-2px] text-[#D4D4D4]'
className='!h-auto !px-1 !py-0 -mx-1 mt-[-2px] text-[var(--text-secondary)]'
onClick={handleClick}
>
Upgrade
@@ -205,6 +205,15 @@ export interface PopoverContentProps
* Maximum height for the popover content in pixels
*/
maxHeight?: number
/**
* Maximum width for the popover content in pixels.
* When provided, Popover will also enable default truncation for inner text and section headers.
*/
maxWidth?: number
/**
* Minimum width for the popover content in pixels
*/
minWidth?: number
/**
* Preferred side to display the popover
* @default 'bottom'
@@ -249,6 +258,8 @@ const PopoverContent = React.forwardRef<
style,
children,
maxHeight,
maxWidth,
minWidth,
side = 'bottom',
align = 'start',
sideOffset,
@@ -264,7 +275,11 @@ const PopoverContent = React.forwardRef<
// When present, we enable default text truncation behavior for inner flexible items,
// so callers don't need to manually pass 'truncate' to every label.
const hasUserWidthConstraint =
style?.minWidth !== undefined || style?.maxWidth !== undefined || style?.width !== undefined
maxWidth !== undefined ||
minWidth !== undefined ||
style?.minWidth !== undefined ||
style?.maxWidth !== undefined ||
style?.width !== undefined
return (
<PopoverPrimitive.Portal>
@@ -278,15 +293,21 @@ const PopoverContent = React.forwardRef<
sticky='partial'
{...restProps}
className={cn(
'z-[10000001] flex flex-col overflow-hidden rounded-[8px] bg-[var(--surface-3)] px-[5.5px] py-[5px] text-foreground outline-none dark:bg-[var(--surface-3)]',
// If width is constrained by the caller, ensure inner flexible text truncates by default.
hasUserWidthConstraint && '[&_.flex-1]:truncate',
className
'z-[10000001] flex flex-col overflow-auto rounded-[8px] bg-[var(--surface-3)] px-[5.5px] py-[5px] text-foreground outline-none dark:bg-[var(--surface-3)]',
// If width is constrained by the caller (prop or style), ensure inner flexible text truncates by default,
// and also truncate section headers.
hasUserWidthConstraint && '[&_.flex-1]:truncate [&_[data-popover-section]]:truncate',
)}
style={{
maxHeight: `${maxHeight || 400}px`,
maxWidth: 'calc(100vw - 16px)',
minWidth: '160px',
maxWidth: maxWidth !== undefined ? `${maxWidth}px` : 'calc(100vw - 16px)',
// Only enforce default min width when the user hasn't set width constraints
minWidth:
minWidth !== undefined
? `${minWidth}px`
: hasUserWidthConstraint
? undefined
: '160px',
...style,
}}
>
@@ -319,7 +340,7 @@ const PopoverScrollArea = React.forwardRef<HTMLDivElement, PopoverScrollAreaProp
({ className, ...props }, ref) => {
return (
<div
className={cn('min-h-0 flex-1 overflow-auto overscroll-contain', className)}
className={cn('min-h-0 overflow-auto overscroll-contain', className)}
ref={ref}
{...props}
/>
@@ -415,9 +436,10 @@ const PopoverSection = React.forwardRef<HTMLDivElement, PopoverSectionProps>(
return (
<div
className={cn(
'px-[6px] py-[4px] font-base text-[12px] text-[var(--text-tertiary)] dark:text-[var(--text-tertiary)]',
'min-w-0 px-[6px] py-[4px] font-base text-[12px] text-[var(--text-tertiary)] dark:text-[var(--text-tertiary)]',
className
)}
data-popover-section=''
ref={ref}
{...props}
/>
+4 -14
View File
@@ -109,7 +109,7 @@ interface ChatState {
}
/**
* Calculate default center position based on available canvas space
* Calculate default position in top right of canvas, 32px from top and right of panel
*/
const calculateDefaultPosition = (): ChatPosition => {
if (typeof window === 'undefined') {
@@ -117,23 +117,13 @@ const calculateDefaultPosition = (): ChatPosition => {
}
// Get current layout dimensions
const sidebarWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
)
const panelWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
)
const terminalHeight = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
)
// Calculate available space
const availableWidth = window.innerWidth - sidebarWidth - panelWidth
const availableHeight = window.innerHeight - terminalHeight
// Center in available space
const x = sidebarWidth + (availableWidth - DEFAULT_WIDTH) / 2
const y = (availableHeight - DEFAULT_HEIGHT) / 2
// Position in top right of canvas, 32px from top and 32px from right of panel
const x = window.innerWidth - panelWidth - 32 - DEFAULT_WIDTH
const y = 32
return { x, y }
}
+1 -1
View File
@@ -25,7 +25,7 @@ interface TerminalState {
* Note: Maximum height is enforced dynamically at 70% of viewport height in the resize hook
*/
const MIN_TERMINAL_HEIGHT = 30
export const DEFAULT_TERMINAL_HEIGHT = 145
export const DEFAULT_TERMINAL_HEIGHT = 196
/**
* Output panel width constraints