mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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>
|
||||
|
||||
+26
-21
@@ -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>
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+11
-1
@@ -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
|
||||
|
||||
+14
-11
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user