Skip to content

Commit e98b1cd

Browse files
chore: split user-input
1 parent 714424e commit e98b1cd

File tree

9 files changed

+619
-528
lines changed

9 files changed

+619
-528
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use client'
2+
3+
import { useEffect } from 'react'
4+
import { useAnimatedPlaceholder } from '@/app/workspace/[workspaceId]/home/hooks'
5+
6+
interface AnimatedPlaceholderEffectProps {
7+
textareaRef: React.RefObject<HTMLTextAreaElement | null>
8+
isInitialView: boolean
9+
}
10+
11+
export function AnimatedPlaceholderEffect({
12+
textareaRef,
13+
isInitialView,
14+
}: AnimatedPlaceholderEffectProps) {
15+
const animatedPlaceholder = useAnimatedPlaceholder(isInitialView)
16+
const placeholder = isInitialView ? animatedPlaceholder : 'Send message to Sim'
17+
18+
useEffect(() => {
19+
if (textareaRef.current) {
20+
textareaRef.current.placeholder = placeholder
21+
}
22+
}, [placeholder, textareaRef])
23+
24+
return null
25+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use client'
2+
3+
import React from 'react'
4+
import { Loader2, X } from 'lucide-react'
5+
import { Tooltip } from '@/components/emcn'
6+
import { getDocumentIcon } from '@/components/icons/document-icons'
7+
import type { AttachedFile } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'
8+
9+
interface AttachedFilesListProps {
10+
attachedFiles: AttachedFile[]
11+
onFileClick: (file: AttachedFile) => void
12+
onRemoveFile: (id: string) => void
13+
}
14+
15+
export const AttachedFilesList = React.memo(function AttachedFilesList({
16+
attachedFiles,
17+
onFileClick,
18+
onRemoveFile,
19+
}: AttachedFilesListProps) {
20+
if (attachedFiles.length === 0) return null
21+
22+
return (
23+
<div className='mb-[6px] flex flex-wrap gap-[6px]'>
24+
{attachedFiles.map((file) => {
25+
const isImage = file.type.startsWith('image/')
26+
return (
27+
<Tooltip.Root key={file.id}>
28+
<Tooltip.Trigger asChild>
29+
<div
30+
className='group relative h-[56px] w-[56px] flex-shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-5)] hover:bg-[var(--surface-4)]'
31+
onClick={() => onFileClick(file)}
32+
>
33+
{isImage && file.previewUrl ? (
34+
<img
35+
src={file.previewUrl}
36+
alt={file.name}
37+
className='h-full w-full object-cover'
38+
/>
39+
) : (
40+
<div className='flex h-full w-full flex-col items-center justify-center gap-[2px] text-[var(--text-icon)]'>
41+
{(() => {
42+
const Icon = getDocumentIcon(file.type, file.name)
43+
return <Icon className='h-[18px] w-[18px]' />
44+
})()}
45+
<span className='max-w-[48px] truncate px-[2px] text-[9px] text-[var(--text-muted)]'>
46+
{file.name.split('.').pop()}
47+
</span>
48+
</div>
49+
)}
50+
{file.uploading && (
51+
<div className='absolute inset-0 flex items-center justify-center bg-black/50'>
52+
<Loader2 className='h-[14px] w-[14px] animate-spin text-white' />
53+
</div>
54+
)}
55+
{!file.uploading && (
56+
<button
57+
type='button'
58+
onClick={(e) => {
59+
e.stopPropagation()
60+
onRemoveFile(file.id)
61+
}}
62+
className='absolute top-[2px] right-[2px] flex h-[16px] w-[16px] items-center justify-center rounded-full bg-black/60 opacity-0 group-hover:opacity-100'
63+
>
64+
<X className='h-[10px] w-[10px] text-white' />
65+
</button>
66+
)}
67+
</div>
68+
</Tooltip.Trigger>
69+
<Tooltip.Content side='top'>
70+
<p className='max-w-[200px] truncate'>{file.name}</p>
71+
</Tooltip.Content>
72+
</Tooltip.Root>
73+
)
74+
})}
75+
</div>
76+
)
77+
})
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { cn } from '@/lib/core/utils/cn'
2+
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
3+
import type { ChatContext } from '@/stores/panel'
4+
5+
export interface SpeechRecognitionEvent extends Event {
6+
resultIndex: number
7+
results: SpeechRecognitionResultList
8+
}
9+
10+
export interface SpeechRecognitionErrorEvent extends Event {
11+
error: string
12+
}
13+
14+
export interface SpeechRecognitionInstance extends EventTarget {
15+
continuous: boolean
16+
interimResults: boolean
17+
lang: string
18+
start(): void
19+
stop(): void
20+
abort(): void
21+
onstart: ((ev: Event) => void) | null
22+
onend: ((ev: Event) => void) | null
23+
onresult: ((ev: SpeechRecognitionEvent) => void) | null
24+
onerror: ((ev: SpeechRecognitionErrorEvent) => void) | null
25+
}
26+
27+
export interface SpeechRecognitionStatic {
28+
new (): SpeechRecognitionInstance
29+
}
30+
31+
export type WindowWithSpeech = Window & {
32+
SpeechRecognition?: SpeechRecognitionStatic
33+
webkitSpeechRecognition?: SpeechRecognitionStatic
34+
}
35+
36+
export interface PlusMenuHandle {
37+
open: () => void
38+
}
39+
40+
export const TEXTAREA_BASE_CLASSES = cn(
41+
'm-0 box-border h-auto min-h-[24px] w-full resize-none',
42+
'overflow-y-auto overflow-x-hidden break-all border-0 bg-transparent',
43+
'px-[4px] py-[4px] font-body text-[15px] leading-[24px] tracking-[-0.015em]',
44+
'text-transparent caret-[var(--text-primary)] outline-none',
45+
'placeholder:font-[380] placeholder:text-[var(--text-subtle)]',
46+
'focus-visible:ring-0 focus-visible:ring-offset-0',
47+
'[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
48+
)
49+
50+
export const OVERLAY_CLASSES = cn(
51+
'pointer-events-none absolute top-0 left-0 m-0 box-border h-auto w-full resize-none',
52+
'overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-all border-0 bg-transparent',
53+
'px-[4px] py-[4px] font-body text-[15px] leading-[24px] tracking-[-0.015em]',
54+
'text-[var(--text-primary)] outline-none',
55+
'[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
56+
)
57+
58+
export const SEND_BUTTON_BASE = 'h-[28px] w-[28px] rounded-full border-0 p-0 transition-colors'
59+
export const SEND_BUTTON_ACTIVE =
60+
'bg-[var(--c-383838)] hover:bg-[var(--c-575757)] dark:bg-[var(--c-E0E0E0)] dark:hover:bg-[var(--c-CFCFCF)]'
61+
export const SEND_BUTTON_DISABLED = 'bg-[var(--c-808080)] dark:bg-[var(--c-808080)]'
62+
63+
export const MAX_CHAT_TEXTAREA_HEIGHT = 200
64+
export const SPEECH_RECOGNITION_LANG = 'en-US'
65+
66+
export function autoResizeTextarea(e: React.FormEvent<HTMLTextAreaElement>, maxHeight: number) {
67+
const target = e.target as HTMLTextAreaElement
68+
target.style.height = 'auto'
69+
target.style.height = `${Math.min(target.scrollHeight, maxHeight)}px`
70+
}
71+
72+
export function mapResourceToContext(resource: MothershipResource): ChatContext {
73+
switch (resource.type) {
74+
case 'workflow':
75+
return {
76+
kind: 'workflow',
77+
workflowId: resource.id,
78+
label: resource.title,
79+
}
80+
case 'knowledgebase':
81+
return {
82+
kind: 'knowledge',
83+
knowledgeId: resource.id,
84+
label: resource.title,
85+
}
86+
case 'table':
87+
return { kind: 'table', tableId: resource.id, label: resource.title }
88+
case 'file':
89+
return { kind: 'file', fileId: resource.id, label: resource.title }
90+
default:
91+
return { kind: 'docs', label: resource.title }
92+
}
93+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client'
2+
3+
import React from 'react'
4+
import {
5+
AudioIcon,
6+
CsvIcon,
7+
DocxIcon,
8+
JsonIcon,
9+
MarkdownIcon,
10+
PdfIcon,
11+
TxtIcon,
12+
VideoIcon,
13+
XlsxIcon,
14+
} from '@/components/icons/document-icons'
15+
16+
const DROP_OVERLAY_ICONS = [
17+
PdfIcon,
18+
DocxIcon,
19+
XlsxIcon,
20+
CsvIcon,
21+
TxtIcon,
22+
MarkdownIcon,
23+
JsonIcon,
24+
AudioIcon,
25+
VideoIcon,
26+
] as const
27+
28+
export const DropOverlay = React.memo(function DropOverlay() {
29+
return (
30+
<div className='pointer-events-none absolute inset-[6px] z-10 flex items-center justify-center rounded-[14px] border-[1.5px] border-[var(--border-1)] border-dashed bg-[var(--white)] dark:bg-[var(--surface-4)]'>
31+
<div className='flex flex-col items-center gap-[8px]'>
32+
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>Drop files</span>
33+
<div className='flex items-center gap-[8px] text-[var(--text-icon)]'>
34+
{DROP_OVERLAY_ICONS.map((Icon, i) => (
35+
<Icon key={i} className='h-[14px] w-[14px]' />
36+
))}
37+
</div>
38+
</div>
39+
</div>
40+
)
41+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export { AnimatedPlaceholderEffect } from './animated-placeholder-effect'
2+
export { AttachedFilesList } from './attached-files-list'
3+
export type {
4+
PlusMenuHandle,
5+
SpeechRecognitionErrorEvent,
6+
SpeechRecognitionEvent,
7+
SpeechRecognitionInstance,
8+
WindowWithSpeech,
9+
} from './constants'
10+
export {
11+
autoResizeTextarea,
12+
MAX_CHAT_TEXTAREA_HEIGHT,
13+
mapResourceToContext,
14+
OVERLAY_CLASSES,
15+
SPEECH_RECOGNITION_LANG,
16+
TEXTAREA_BASE_CLASSES,
17+
} from './constants'
18+
export { DropOverlay } from './drop-overlay'
19+
export { MicButton } from './mic-button'
20+
export type { AvailableResourceGroup } from './plus-menu-dropdown'
21+
export { PlusMenuDropdown } from './plus-menu-dropdown'
22+
export { SendButton } from './send-button'
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use client'
2+
3+
import React from 'react'
4+
import { Mic } from 'lucide-react'
5+
import { cn } from '@/lib/core/utils/cn'
6+
7+
interface MicButtonProps {
8+
isListening: boolean
9+
onToggle: () => void
10+
}
11+
12+
export const MicButton = React.memo(function MicButton({ isListening, onToggle }: MicButtonProps) {
13+
return (
14+
<button
15+
type='button'
16+
onClick={onToggle}
17+
className={cn(
18+
'flex h-[28px] w-[28px] items-center justify-center rounded-full transition-colors',
19+
isListening
20+
? 'bg-red-500 text-white hover:bg-red-600'
21+
: 'text-[var(--text-icon)] hover:bg-[#F7F7F7] dark:hover:bg-[#303030]'
22+
)}
23+
title={isListening ? 'Stop listening' : 'Voice input'}
24+
>
25+
<Mic className='h-[16px] w-[16px]' strokeWidth={2} />
26+
</button>
27+
)
28+
})

0 commit comments

Comments
 (0)