Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions assets/src/components/ai/agent-runs/AIAgentRuntimesSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Chip,
CloseIcon,
ListBoxFooterPlus,
ListBoxItem,
Expand Down Expand Up @@ -56,17 +57,22 @@ export function AIAgentRuntimesSelector({
if (data && !selectedRuntimeId) setRuntimeToDefault()
}, [data, selectedRuntimeId])
return (
<div css={{ width: type === 'standard' ? 240 : undefined, ...outerStyles }}>
<div css={{ width: type === 'standard' ? 280 : undefined, ...outerStyles }}>
<Select
transparent
isOpen={isOpen}
onOpenChange={setIsOpen}
width={type === 'standard' ? 240 : 180}
width={type === 'standard' ? 280 : 240}
placement="left"
label={isLoading ? <RectangleSkeleton /> : placeholder}
leftContent={
isLoading || !selectedRuntime ? undefined : <SelectedIcon fullColor />
}
rightContent={
type === 'standard' ? (
<AgentRuntimeModelChip model={selectedRuntime?.model?.model} />
) : undefined
}
selectedKey={selectedRuntimeId ?? ''}
onSelectionChange={(key) => setSelectedRuntimeId(key ? `${key}` : null)}
dropdownFooterFixed={
Expand Down Expand Up @@ -116,17 +122,33 @@ export function AIAgentRuntimesSelector({
}
{...props}
>
{runtimes.map(({ id, name, type }) => {
const Icon = runtimeToIcon[type]
{runtimes.map((runtime) => {
const Icon = runtimeToIcon[runtime.type]
return (
<ListBoxItem
key={id}
label={name}
key={runtime.id}
label={runtime.name}
leftContent={<Icon fullColor />}
rightContent={
<AgentRuntimeModelChip model={runtime.model?.model} />
}
/>
)
})}
</Select>
</div>
)
}

function AgentRuntimeModelChip({ model }: { model?: Nullable<string> }) {
if (!model) return null

return (
<Chip
size="small"
severity="neutral"
>
{model}
</Chip>
)
}
82 changes: 12 additions & 70 deletions assets/src/components/ai/chatbot/ChatbotButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import {
ChatOutlineIcon,
} from '@pluralsh/design-system'
import { useAIEnabled } from 'components/contexts/DeploymentSettingsContext.tsx'
import {
borderShimmerStyles,
useBorderShimmer,
} from 'components/utils/borderShimmer'
import { useWorkbenchOptions } from 'components/workbenches/useWorkbenchOptions.ts'
import { AiInsightFragment, AiRole, ChatMessage } from 'generated/graphql.ts'
import { ComponentPropsWithRef, useEffect, useState } from 'react'
import { ComponentPropsWithRef } from 'react'
import { Link } from 'react-router-dom'
import { AI_ABS_PATH } from 'routes/aiRoutesConsts.tsx'
import styled from 'styled-components'
Expand All @@ -16,15 +20,10 @@ import { useChatbot } from '../AIContext.tsx'
const FIX_PREFACE =
"The following is an insight into an issue on the user's infrastructure we'd like to learn more about:"

const ANIMATION_SPEED_S = 4
// run every 20 minutes for 6 seconds
const ANIMATION_ON_MS = 6_000
const ANIMATION_PERIOD_MS = 20 * 60 * 1000

export function MainChatbotButton({
...props
}: ComponentPropsWithRef<typeof Button>) {
const showAnimation = usePeriodicPulse(ANIMATION_ON_MS, ANIMATION_PERIOD_MS)
const showAnimation = useBorderShimmer()
return (
<MainChatbotButtonSC
$showAnimation={showAnimation}
Expand Down Expand Up @@ -104,67 +103,10 @@ export function ChatWithAIButton({
}

const MainChatbotButtonSC = styled(Button)<{ $showAnimation: boolean }>(
({ theme, $showAnimation }) => ({
'&, &:hover, &:focus': {
'@property --border-angle-1': {
syntax: "'<angle>'",
inherits: 'true',
initialValue: '0deg',
},
'@property --border-angle-2': {
syntax: "'<angle>'",
inherits: 'true',
initialValue: '180deg',
},
'--border-angle-1': '0deg',
'--border-angle-2': '180deg',
border: $showAnimation ? '1px solid transparent' : undefined,
backgroundImage: `
linear-gradient(${theme.colors['fill-accent']}, ${theme.colors['fill-accent']}),
conic-gradient(
from var(--border-angle-1) at 25% 30%,
transparent,
${theme.colors['border-outline-focused']} 12%,
transparent 32%,
transparent
),
conic-gradient(
from var(--border-angle-2) at 75% 60%,
transparent,
${theme.colors['border-input']} 12%,
transparent 60%,
transparent
)
`,
backgroundClip: 'padding-box, border-box, border-box',
backgroundOrigin: 'border-box',
animation: `rotateChatBtnBorderA ${ANIMATION_SPEED_S}s linear infinite, rotateChatBtnBorderB ${ANIMATION_SPEED_S * 1.5}s linear infinite`,
animationPlayState: $showAnimation ? 'running' : 'paused',
'@keyframes rotateChatBtnBorderA': {
to: { '--border-angle-1': '360deg' },
},
'@keyframes rotateChatBtnBorderB': {
to: { '--border-angle-2': '-360deg' },
},
},
})
({ theme, $showAnimation }) =>
borderShimmerStyles({
theme,
showAnimation: $showAnimation,
fillColor: theme.colors['fill-accent'],
})
)

function usePeriodicPulse(onMs: number, periodMs: number) {
const [on, setOn] = useState(false)
useEffect(() => {
let timeoutId: NodeJS.Timeout
const trigger = () => {
if (timeoutId) clearTimeout(timeoutId)
setOn(true)
timeoutId = setTimeout(() => setOn(false), onMs)
}
// summed so the timing starts at the end of the animation
const intervalId = setInterval(trigger, periodMs + onMs)
return () => {
clearInterval(intervalId)
clearTimeout(timeoutId)
}
}, [onMs, periodMs])
return on
}
Loading
Loading