Compare commits
6 Commits
v1.6.1
...
feat/messa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7e79353fc | ||
|
|
93e972a5da | ||
|
|
12d08e4748 | ||
|
|
52a980f751 | ||
|
|
3b7ab2aec8 | ||
|
|
d41e239b89 |
@@ -134,9 +134,10 @@ export async function buildStreamTextParams(
|
||||
if (aiSdkProviderId === 'google-vertex') {
|
||||
tools.google_search = vertex.tools.googleSearch({}) as ProviderDefinedTool
|
||||
} else if (aiSdkProviderId === 'google-vertex-anthropic') {
|
||||
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
tools.web_search = vertexAnthropic.tools.webSearch_20250305({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
}) as ProviderDefinedTool
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +61,10 @@ export function buildProviderBuiltinWebSearchConfig(
|
||||
}
|
||||
}
|
||||
case 'anthropic': {
|
||||
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
const anthropicSearchOptions: AnthropicSearchConfig = {
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
}
|
||||
return {
|
||||
anthropic: anthropicSearchOptions
|
||||
|
||||
@@ -163,6 +163,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@keyframes fadeInWithBlur {
|
||||
from { opacity: 0; filter: blur(2px); }
|
||||
to { opacity: 1; filter: blur(0px); }
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
@@ -457,7 +457,13 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
|
||||
// 面板可见且未折叠时:拦截所有 Enter 变体;
|
||||
// 纯 Enter 选择项,带修饰键仅拦截不处理
|
||||
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
|
||||
if (e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
// Don't prevent default or stop propagation - let it create a newline
|
||||
setIsMouseOver(false)
|
||||
break
|
||||
}
|
||||
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsMouseOver(false)
|
||||
|
||||
@@ -87,6 +87,9 @@ const CommandListPopover = ({
|
||||
return true
|
||||
|
||||
case 'Enter':
|
||||
if (event.shiftKey) {
|
||||
return false
|
||||
}
|
||||
event.preventDefault()
|
||||
if (items[internalSelectedIndex]) {
|
||||
selectItem(internalSelectedIndex)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { commandSuggestion } from '../command'
|
||||
|
||||
describe('commandSuggestion render', () => {
|
||||
it('has render function', () => {
|
||||
expect(commandSuggestion.render).toBeDefined()
|
||||
expect(typeof commandSuggestion.render).toBe('function')
|
||||
})
|
||||
|
||||
it('render function returns object with onKeyDown', () => {
|
||||
const renderResult = commandSuggestion.render?.()
|
||||
expect(renderResult).toBeDefined()
|
||||
expect(renderResult?.onKeyDown).toBeDefined()
|
||||
expect(typeof renderResult?.onKeyDown).toBe('function')
|
||||
})
|
||||
})
|
||||
@@ -628,13 +628,34 @@ export const commandSuggestion: Omit<SuggestionOptions<Command, MentionNodeAttrs
|
||||
},
|
||||
|
||||
onKeyDown: (props) => {
|
||||
// Let CommandListPopover handle events first
|
||||
const popoverHandled = component.ref?.onKeyDown?.(props.event)
|
||||
if (popoverHandled) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle Shift+Enter for newline when popover doesn't handle it
|
||||
if (props.event.key === 'Enter' && props.event.shiftKey) {
|
||||
props.event.preventDefault()
|
||||
// Close the suggestion menu
|
||||
if (cleanup) cleanup()
|
||||
component.destroy()
|
||||
// Use the view from SuggestionKeyDownProps to insert newline
|
||||
const { view } = props
|
||||
const { state, dispatch } = view
|
||||
const { tr } = state
|
||||
tr.insertText('\n')
|
||||
dispatch(tr)
|
||||
return true
|
||||
}
|
||||
|
||||
if (props.event.key === 'Escape') {
|
||||
if (cleanup) cleanup()
|
||||
component.destroy()
|
||||
return true
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props.event)
|
||||
return false
|
||||
},
|
||||
|
||||
onExit: () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
// Default quick assistant model
|
||||
glm45FlashModel
|
||||
],
|
||||
cherryin: [],
|
||||
// cherryin: [],
|
||||
vertexai: [],
|
||||
'302ai': [
|
||||
{
|
||||
|
||||
@@ -78,16 +78,16 @@ export const CHERRYAI_PROVIDER: SystemProvider = {
|
||||
}
|
||||
|
||||
export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> = {
|
||||
cherryin: {
|
||||
id: 'cherryin',
|
||||
name: 'CherryIN',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.cherryin.ai',
|
||||
models: [],
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
},
|
||||
// cherryin: {
|
||||
// id: 'cherryin',
|
||||
// name: 'CherryIN',
|
||||
// type: 'openai',
|
||||
// apiKey: '',
|
||||
// apiHost: 'https://open.cherryin.ai',
|
||||
// models: [],
|
||||
// isSystem: true,
|
||||
// enabled: true
|
||||
// },
|
||||
silicon: {
|
||||
id: 'silicon',
|
||||
name: 'Silicon',
|
||||
@@ -708,17 +708,17 @@ type ProviderUrls = {
|
||||
}
|
||||
|
||||
export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
cherryin: {
|
||||
api: {
|
||||
url: 'https://open.cherryin.ai'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://open.cherryin.ai',
|
||||
apiKey: 'https://open.cherryin.ai/console/token',
|
||||
docs: 'https://open.cherryin.ai',
|
||||
models: 'https://open.cherryin.ai/pricing'
|
||||
}
|
||||
},
|
||||
// cherryin: {
|
||||
// api: {
|
||||
// url: 'https://open.cherryin.ai'
|
||||
// },
|
||||
// websites: {
|
||||
// official: 'https://open.cherryin.ai',
|
||||
// apiKey: 'https://open.cherryin.ai/console/token',
|
||||
// docs: 'https://open.cherryin.ai',
|
||||
// models: 'https://open.cherryin.ai/pricing'
|
||||
// }
|
||||
// },
|
||||
ph8: {
|
||||
api: {
|
||||
url: 'https://ph8.co'
|
||||
|
||||
@@ -12,6 +12,7 @@ import { removeSvgEmptyLines } from '@renderer/utils/formats'
|
||||
import { processLatexBrackets } from '@renderer/utils/markdown'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { type FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
@@ -64,6 +65,8 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
||||
initialText: block.content
|
||||
})
|
||||
|
||||
const isStreaming = block.status === 'streaming'
|
||||
|
||||
useEffect(() => {
|
||||
const newContent = block.content || ''
|
||||
const oldContent = prevContentRef.current || ''
|
||||
@@ -85,9 +88,8 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
||||
prevBlockIdRef.current = block.id
|
||||
|
||||
// 更新 stream 状态
|
||||
const isStreaming = block.status === 'streaming'
|
||||
setIsStreamDone(!isStreaming)
|
||||
}, [block.content, block.id, block.status, addChunk, reset])
|
||||
}, [block.content, block.id, block.status, addChunk, reset, isStreaming])
|
||||
|
||||
const remarkPlugins = useMemo(() => {
|
||||
const plugins = [
|
||||
@@ -130,14 +132,16 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
||||
table: (props: any) => <Table {...props} blockId={block.id} />,
|
||||
img: (props: any) => <ImageViewer style={{ maxWidth: 500, maxHeight: 500 }} {...props} />,
|
||||
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />,
|
||||
p: (props) => {
|
||||
p: SmoothFade((props) => {
|
||||
const hasImage = props?.node?.children?.some((child: any) => child.tagName === 'img')
|
||||
if (hasImage) return <div {...props} />
|
||||
return <p {...props} />
|
||||
},
|
||||
svg: MarkdownSvgRenderer
|
||||
}, isStreaming),
|
||||
svg: MarkdownSvgRenderer,
|
||||
li: SmoothFade((props) => <li {...props} />, isStreaming),
|
||||
span: SmoothFade((props) => <span {...props} />, isStreaming)
|
||||
} as Partial<Components>
|
||||
}, [block.id])
|
||||
}, [block.id, isStreaming])
|
||||
|
||||
if (/<style\b[^>]*>/i.test(messageContent)) {
|
||||
components.style = MarkdownShadowDOMRenderer as any
|
||||
@@ -168,3 +172,23 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
||||
}
|
||||
|
||||
export default memo(Markdown)
|
||||
|
||||
const SmoothFade = (Comp: React.ElementType, isStreaming: boolean) => {
|
||||
const handleAnimationEnd = (e: React.AnimationEvent) => {
|
||||
// 动画结束后移除类名
|
||||
if (e.animationName === 'fadeInWithBlur') {
|
||||
e.currentTarget.classList.remove('animate-[fadeInWithBlur_500ms_ease-out_forwards]')
|
||||
e.currentTarget.classList.remove('opacity-0')
|
||||
}
|
||||
}
|
||||
return ({ children, ...rest }) => {
|
||||
return (
|
||||
<Comp
|
||||
{...rest}
|
||||
className={isStreaming ? 'animate-[fadeInWithBlur_500ms_ease-out_forwards] opacity-0' : ''}
|
||||
onAnimationEnd={handleAnimationEnd}>
|
||||
{children}
|
||||
</Comp>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import { Spinner } from '@heroui/react'
|
||||
import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage'
|
||||
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
||||
import { Loader } from '@renderer/ui/loader'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface PlaceholderBlockProps {
|
||||
block: PlaceholderMessageBlock
|
||||
status: MessageBlockStatus
|
||||
type: MessageBlockType
|
||||
}
|
||||
const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ block }) => {
|
||||
if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) {
|
||||
const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ status, type }) => {
|
||||
if (status === MessageBlockStatus.PROCESSING && type === MessageBlockType.UNKNOWN) {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
<Spinner color="current" variant="dots" />
|
||||
</MessageContentLoading>
|
||||
<div className="-mt-2">
|
||||
<Loader variant="terminal" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
const MessageContentLoading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
margin-top: -5px;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
export default React.memo(PlaceholderBlock)
|
||||
|
||||
@@ -215,15 +215,7 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
|
||||
})}
|
||||
{isProcessing && (
|
||||
<AnimatedBlockWrapper key="message-loading-placeholder" enableAnimation={true}>
|
||||
<PlaceholderBlock
|
||||
block={{
|
||||
id: `loading-${message.id}`,
|
||||
messageId: message.id,
|
||||
type: MessageBlockType.UNKNOWN,
|
||||
status: MessageBlockStatus.PROCESSING,
|
||||
createdAt: new Date().toISOString()
|
||||
}}
|
||||
/>
|
||||
<PlaceholderBlock type={MessageBlockType.UNKNOWN} status={MessageBlockStatus.PROCESSING} />
|
||||
</AnimatedBlockWrapper>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 157,
|
||||
version: 158,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -2539,6 +2539,15 @@ const migrateConfig = {
|
||||
logger.error('migrate 157 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'158': (state: RootState) => {
|
||||
try {
|
||||
state.llm.providers = state.llm.providers.filter((provider) => provider.id !== 'cherryin')
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 158 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ export type Provider = {
|
||||
}
|
||||
|
||||
export const SystemProviderIds = {
|
||||
cherryin: 'cherryin',
|
||||
// cherryin: 'cherryin',
|
||||
silicon: 'silicon',
|
||||
aihubmix: 'aihubmix',
|
||||
ocoolai: 'ocoolai',
|
||||
|
||||
374
src/renderer/src/ui/loader.tsx
Normal file
374
src/renderer/src/ui/loader.tsx
Normal file
@@ -0,0 +1,374 @@
|
||||
import { cn } from '@renderer/utils/index'
|
||||
|
||||
export interface LoaderProps {
|
||||
variant?:
|
||||
| 'circular'
|
||||
| 'classic'
|
||||
| 'pulse'
|
||||
| 'pulse-dot'
|
||||
| 'dots'
|
||||
| 'typing'
|
||||
| 'wave'
|
||||
| 'bars'
|
||||
| 'terminal'
|
||||
| 'text-blink'
|
||||
| 'text-shimmer'
|
||||
| 'loading-dots'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
text?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CircularLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||
const sizeClasses = {
|
||||
sm: 'size-4',
|
||||
md: 'size-5',
|
||||
lg: 'size-6'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'animate-spin rounded-full border-2 border-primary border-t-transparent',
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}>
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ClassicLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||
const sizeClasses = {
|
||||
sm: 'size-4',
|
||||
md: 'size-5',
|
||||
lg: 'size-6'
|
||||
}
|
||||
|
||||
const barSizes = {
|
||||
sm: { height: '6px', width: '1.5px' },
|
||||
md: { height: '8px', width: '2px' },
|
||||
lg: { height: '10px', width: '2.5px' }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative', sizeClasses[size], className)}>
|
||||
<div className="absolute h-full w-full">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute animate-[spinner-fade_1.2s_linear_infinite] rounded-full bg-primary"
|
||||
style={{
|
||||
top: '0',
|
||||
left: '50%',
|
||||
marginLeft: size === 'sm' ? '-0.75px' : size === 'lg' ? '-1.25px' : '-1px',
|
||||
transformOrigin: `${size === 'sm' ? '0.75px' : size === 'lg' ? '1.25px' : '1px'} ${size === 'sm' ? '10px' : size === 'lg' ? '14px' : '12px'}`,
|
||||
transform: `rotate(${i * 30}deg)`,
|
||||
opacity: 0,
|
||||
animationDelay: `${i * 0.1}s`,
|
||||
height: barSizes[size].height,
|
||||
width: barSizes[size].width
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PulseLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||
const sizeClasses = {
|
||||
sm: 'size-4',
|
||||
md: 'size-5',
|
||||
lg: 'size-6'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative', sizeClasses[size], className)}>
|
||||
<div className="absolute inset-0 animate-[thin-pulse_1.5s_ease-in-out_infinite] rounded-full border-2 border-primary" />
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PulseDotLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||
const sizeClasses = {
|
||||
sm: 'size-1',
|
||||
md: 'size-2',
|
||||
lg: 'size-3'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'animate-[pulse-dot_1.2s_ease-in-out_infinite] rounded-full bg-primary',
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}>
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DotsLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||
const dotSizes = {
|
||||
sm: 'h-1.5 w-1.5',
|
||||
md: 'h-2 w-2',
|
||||
lg: 'h-2.5 w-2.5'
|
||||
}
|
||||
|
||||
const containerSizes = {
|
||||
sm: 'h-4',
|
||||
md: 'h-5',
|
||||
lg: 'h-6'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center space-x-1', containerSizes[size], className)}>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn('animate-[bounce-dots_1.4s_ease-in-out_infinite] rounded-full bg-primary', dotSizes[size])}
|
||||
style={{
|
||||
animationDelay: `${i * 160}ms`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TypingLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||
const dotSizes = {
|
||||
sm: 'h-1 w-1',
|
||||
md: 'h-1.5 w-1.5',
|
||||
lg: 'h-2 w-2'
|
||||
}
|
||||
|
||||
const containerSizes = {
|
||||
sm: 'h-4',
|
||||
md: 'h-5',
|
||||
lg: 'h-6'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center space-x-1', containerSizes[size], className)}>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn('animate-[typing_1s_infinite] rounded-full bg-primary', dotSizes[size])}
|
||||
style={{
|
||||
animationDelay: `${i * 250}ms`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WaveLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||
const barWidths = {
|
||||
sm: 'w-0.5',
|
||||
md: 'w-0.5',
|
||||
lg: 'w-1'
|
||||
}
|
||||
|
||||
const containerSizes = {
|
||||
sm: 'h-4',
|
||||
md: 'h-5',
|
||||
lg: 'h-6'
|
||||
}
|
||||
|
||||
const heights = {
|
||||
sm: ['6px', '9px', '12px', '9px', '6px'],
|
||||
md: ['8px', '12px', '16px', '12px', '8px'],
|
||||
lg: ['10px', '15px', '20px', '15px', '10px']
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-0.5', containerSizes[size], className)}>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn('animate-[wave_1s_ease-in-out_infinite] rounded-full bg-primary', barWidths[size])}
|
||||
style={{
|
||||
animationDelay: `${i * 100}ms`,
|
||||
height: heights[size][i]
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function BarsLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||
const barWidths = {
|
||||
sm: 'w-1',
|
||||
md: 'w-1.5',
|
||||
lg: 'w-2'
|
||||
}
|
||||
|
||||
const containerSizes = {
|
||||
sm: 'h-4 gap-1',
|
||||
md: 'h-5 gap-1.5',
|
||||
lg: 'h-6 gap-2'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex', containerSizes[size], className)}>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn('h-full animate-[wave-bars_1.2s_ease-in-out_infinite] bg-primary', barWidths[size])}
|
||||
style={{
|
||||
animationDelay: `${i * 0.2}s`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TerminalLoader({ className, size = 'md' }: { className?: string; size?: 'sm' | 'md' | 'lg' }) {
|
||||
const cursorSizes = {
|
||||
sm: 'h-3 w-1.5',
|
||||
md: 'h-4 w-2',
|
||||
lg: 'h-5 w-2.5'
|
||||
}
|
||||
|
||||
const textSizes = {
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base'
|
||||
}
|
||||
|
||||
const containerSizes = {
|
||||
sm: 'h-4',
|
||||
md: 'h-5',
|
||||
lg: 'h-6'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center space-x-1', containerSizes[size], className)}>
|
||||
<span className={cn('font-mono text-primary', textSizes[size])}>{'>'}</span>
|
||||
<div className={cn('animate-[blink_1s_step-end_infinite] bg-primary', cursorSizes[size])} />
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TextBlinkLoader({
|
||||
text = 'Thinking',
|
||||
className,
|
||||
size = 'md'
|
||||
}: {
|
||||
text?: string
|
||||
className?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}) {
|
||||
const textSizes = {
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('animate-[text-blink_2s_ease-in-out_infinite] font-medium', textSizes[size], className)}>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TextShimmerLoader({
|
||||
text = 'Thinking',
|
||||
className,
|
||||
size = 'md'
|
||||
}: {
|
||||
text?: string
|
||||
className?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}) {
|
||||
const textSizes = {
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-[linear-gradient(to_right,var(--muted-foreground)_40%,var(--foreground)_60%,var(--muted-foreground)_80%)]',
|
||||
'bg-[200%_auto] bg-clip-text font-medium text-transparent',
|
||||
'animate-[shimmer_4s_infinite_linear]',
|
||||
textSizes[size],
|
||||
className
|
||||
)}>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TextDotsLoader({
|
||||
className,
|
||||
text = 'Thinking',
|
||||
size = 'md'
|
||||
}: {
|
||||
className?: string
|
||||
text?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}) {
|
||||
const textSizes = {
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('inline-flex items-center', className)}>
|
||||
<span className={cn('font-medium text-primary', textSizes[size])}>{text}</span>
|
||||
<span className="inline-flex">
|
||||
<span className="animate-[loading-dots_1.4s_infinite_0.2s] text-primary">.</span>
|
||||
<span className="animate-[loading-dots_1.4s_infinite_0.4s] text-primary">.</span>
|
||||
<span className="animate-[loading-dots_1.4s_infinite_0.6s] text-primary">.</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Loader({ variant = 'circular', size = 'md', text, className }: LoaderProps) {
|
||||
switch (variant) {
|
||||
case 'circular':
|
||||
return <CircularLoader size={size} className={className} />
|
||||
case 'classic':
|
||||
return <ClassicLoader size={size} className={className} />
|
||||
case 'pulse':
|
||||
return <PulseLoader size={size} className={className} />
|
||||
case 'pulse-dot':
|
||||
return <PulseDotLoader size={size} className={className} />
|
||||
case 'dots':
|
||||
return <DotsLoader size={size} className={className} />
|
||||
case 'typing':
|
||||
return <TypingLoader size={size} className={className} />
|
||||
case 'wave':
|
||||
return <WaveLoader size={size} className={className} />
|
||||
case 'bars':
|
||||
return <BarsLoader size={size} className={className} />
|
||||
case 'terminal':
|
||||
return <TerminalLoader size={size} className={className} />
|
||||
case 'text-blink':
|
||||
return <TextBlinkLoader text={text} size={size} className={className} />
|
||||
case 'text-shimmer':
|
||||
return <TextShimmerLoader text={text} size={size} className={className} />
|
||||
case 'loading-dots':
|
||||
return <TextDotsLoader text={text} size={size} className={className} />
|
||||
default:
|
||||
return <CircularLoader size={size} className={className} />
|
||||
}
|
||||
}
|
||||
|
||||
export { Loader }
|
||||
@@ -234,3 +234,4 @@ export * from './match'
|
||||
export * from './naming'
|
||||
export * from './sort'
|
||||
export * from './style'
|
||||
export { cn } from '@heroui/react'
|
||||
|
||||
Reference in New Issue
Block a user