sparkdesign 0.0.1 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -4
- package/dist/commands/add.js +93 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/init.js +96 -0
- package/dist/commands/list.js +25 -0
- package/dist/index.js +37 -0
- package/dist/utils/config.js +53 -0
- package/dist/utils/registry.js +34 -0
- package/dist/utils/tokens.js +176 -0
- package/dist/utils/transform.js +19 -0
- package/package.json +33 -10
- package/registry/__tests__/basic/button.test.tsx +333 -0
- package/registry/__tests__/chat/markdown.test.tsx +387 -0
- package/registry/__tests__/chat/thinking-indicator.test.tsx +244 -0
- package/registry/__tests__/chat/tool-invocation-card.test.tsx +346 -0
- package/registry/basic/alert-dialog.tsx +180 -0
- package/registry/basic/avatar.tsx +120 -0
- package/registry/basic/button.tsx +100 -0
- package/registry/basic/collapse.tsx +94 -0
- package/registry/basic/collapsible-card.tsx +230 -0
- package/registry/basic/collapsible.tsx +21 -0
- package/registry/basic/dropdown-menu.tsx +254 -0
- package/registry/basic/icon-button.tsx +66 -0
- package/registry/basic/icons-inline.tsx +206 -0
- package/registry/basic/kbd.tsx +50 -0
- package/registry/basic/option-list.tsx +125 -0
- package/registry/basic/pagination.tsx +132 -0
- package/registry/basic/progress.tsx +42 -0
- package/registry/basic/radio-group.tsx +69 -0
- package/registry/basic/resizable.tsx +67 -0
- package/registry/basic/scrollbar.tsx +114 -0
- package/registry/basic/select.tsx +177 -0
- package/registry/basic/shimmering-text.tsx +115 -0
- package/registry/basic/sidebar-menu.tsx +177 -0
- package/registry/basic/skeleton.tsx +33 -0
- package/registry/basic/slider.tsx +55 -0
- package/registry/basic/sonner.tsx +104 -0
- package/registry/basic/spinner.tsx +17 -0
- package/registry/basic/switch.tsx +49 -0
- package/registry/basic/table.tsx +117 -0
- package/registry/basic/tabs.tsx +85 -0
- package/registry/basic/tag.tsx +161 -0
- package/registry/basic/theme-from-document.ts +10 -0
- package/registry/basic/toggle.tsx +223 -0
- package/registry/basic/tooltip.tsx +80 -0
- package/registry/basic/typography.tsx +201 -0
- package/registry/chat/ask-user-part.tsx +70 -0
- package/registry/chat/browser-action-part.tsx +166 -0
- package/registry/chat/chat-input/chat-input-folder-selector.tsx +185 -0
- package/registry/chat/chat-input/chat-input-model-switcher.tsx +131 -0
- package/registry/chat/chat-input/chat-input-textarea.tsx +67 -0
- package/registry/chat/chat-input/compound.tsx +334 -0
- package/registry/chat/chat-input/context.tsx +189 -0
- package/registry/chat/chat-input/folder-permission-dialog.tsx +61 -0
- package/registry/chat/chat-input/index.tsx +123 -0
- package/registry/chat/chat-input/types.ts +77 -0
- package/registry/chat/chat-input/useAutoResizeTextarea.ts +20 -0
- package/registry/chat/code-block-part.tsx +151 -0
- package/registry/chat/file-attachment.tsx +44 -0
- package/registry/chat/file-card.tsx +68 -0
- package/registry/chat/file-review-part.tsx +259 -0
- package/registry/chat/folder-button.tsx +169 -0
- package/registry/chat/generated-images-grid.tsx +56 -0
- package/registry/chat/generation-status-bar.tsx +72 -0
- package/registry/chat/hint-banner.tsx +165 -0
- package/registry/chat/image-attachment.tsx +166 -0
- package/registry/chat/image-generating.tsx +281 -0
- package/registry/chat/markdown.tsx +146 -0
- package/registry/chat/mermaid-part.tsx +90 -0
- package/registry/chat/permission-card.tsx +178 -0
- package/registry/chat/plan-part.tsx +168 -0
- package/registry/chat/queue-indicator.tsx +234 -0
- package/registry/chat/reasoning-step/compound.tsx +336 -0
- package/registry/chat/reasoning-step/context.tsx +114 -0
- package/registry/chat/reasoning-step/index.tsx +45 -0
- package/registry/chat/reasoning-step/types.ts +109 -0
- package/registry/chat/related-prompts.tsx +91 -0
- package/registry/chat/response/compound.tsx +210 -0
- package/registry/chat/response/context.tsx +200 -0
- package/registry/chat/response/index.tsx +87 -0
- package/registry/chat/response/types.ts +123 -0
- package/registry/chat/send-button.tsx +94 -0
- package/registry/chat/streaming-markdown-block.tsx +111 -0
- package/registry/chat/task-part.tsx +109 -0
- package/registry/chat/terminal-code-block-part.tsx +69 -0
- package/registry/chat/thinking-indicator.tsx +91 -0
- package/registry/chat/tool-invocation-card.tsx +132 -0
- package/registry/chat/user-message.tsx +38 -0
- package/registry/chat/user-question/UserQuestionCard.tsx +198 -0
- package/registry/chat/user-question/UserQuestionFooter.tsx +66 -0
- package/registry/chat/user-question/UserQuestionHeader.tsx +64 -0
- package/registry/chat/user-question/compound.tsx +324 -0
- package/registry/chat/user-question/context.tsx +456 -0
- package/registry/chat/user-question/index.tsx +95 -0
- package/registry/chat/user-question/types.ts +61 -0
- package/registry/chat/user-question/useUserQuestionKeyboard.ts +126 -0
- package/registry/chat/user-question/useUserQuestionState.ts +165 -0
- package/registry/chat/user-question-answer.tsx +62 -0
- package/registry/lib/file-icon-maps.ts +150 -0
- package/registry/lib/use-mermaid-render.ts +76 -0
- package/registry/lib/utils.ts +6 -0
- package/registry/meta.json +1 -0
- package/registry/tokens/index.css +31 -0
- package/registry/tokens/scale/computed.css +103 -0
- package/registry/tokens/scale/config.css +110 -0
- package/registry/tokens/scale/index.css +30 -0
- package/registry/tokens/scale/presets/compact.css +30 -0
- package/registry/tokens/scale/presets/dense.css +64 -0
- package/registry/tokens/scale/presets/sharp.css +40 -0
- package/registry/tokens/scale/presets/soft.css +16 -0
- package/registry/tokens/scale.css +13 -0
- package/registry/tokens/scrollbar-utility.css +35 -0
- package/registry/tokens/theme.css +633 -0
- package/registry/tokens/themes/dark-parchment.css +132 -0
- package/registry/tokens/themes/dark-qoder.css +132 -0
- package/registry/tokens/themes/light-parchment.css +123 -0
- package/registry/tokens/themes/light-qoder.css +131 -0
- package/index.js +0 -5
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { memo, useState } from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import { TimeLine, CheckboxCircleLine, CloseCircleLine, ArrowRightLine } from '../basic/icons-inline'
|
|
5
|
+
import { Scrollbar } from '../basic/scrollbar'
|
|
6
|
+
|
|
7
|
+
export type ToolInvocationStatus = 'running' | 'success' | 'error'
|
|
8
|
+
|
|
9
|
+
export interface ToolInvocationCardProps {
|
|
10
|
+
toolName: string
|
|
11
|
+
params?: string
|
|
12
|
+
status: ToolInvocationStatus
|
|
13
|
+
response?: string
|
|
14
|
+
defaultExpanded?: boolean
|
|
15
|
+
statusText?: string
|
|
16
|
+
className?: string
|
|
17
|
+
timeIcon?: ReactNode
|
|
18
|
+
checkboxCircleIcon?: ReactNode
|
|
19
|
+
closeCircleIcon?: ReactNode
|
|
20
|
+
arrowRightIcon?: ReactNode
|
|
21
|
+
runningText?: string
|
|
22
|
+
ranText?: string
|
|
23
|
+
failedText?: string
|
|
24
|
+
responseLabel?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* 左侧状态图标随 token:容器 text-xs 使 1em = var(--font-size-xs),图标 size-[1em] 继承 */
|
|
28
|
+
const statusIconClass = 'size-[1em] shrink-0'
|
|
29
|
+
const timeClass = `${statusIconClass} text-text-tertiary`
|
|
30
|
+
const checkboxClass = `${statusIconClass} text-success`
|
|
31
|
+
const closeCircleClass = `${statusIconClass} text-text-tertiary`
|
|
32
|
+
const arrowClass = 'w-3.5 h-3.5 text-text-tertiary transition-all duration-200'
|
|
33
|
+
|
|
34
|
+
export const ToolInvocationCard = memo(function ToolInvocationCard({
|
|
35
|
+
toolName,
|
|
36
|
+
params,
|
|
37
|
+
status,
|
|
38
|
+
response,
|
|
39
|
+
defaultExpanded = false,
|
|
40
|
+
statusText: statusTextProp,
|
|
41
|
+
className,
|
|
42
|
+
timeIcon,
|
|
43
|
+
checkboxCircleIcon,
|
|
44
|
+
closeCircleIcon,
|
|
45
|
+
arrowRightIcon,
|
|
46
|
+
runningText = 'Running',
|
|
47
|
+
ranText = 'Ran',
|
|
48
|
+
failedText = 'Failed',
|
|
49
|
+
responseLabel = 'Response',
|
|
50
|
+
}: ToolInvocationCardProps) {
|
|
51
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
|
|
52
|
+
|
|
53
|
+
const hasResponse = response != null && response.length > 0
|
|
54
|
+
const isRunning = status === 'running'
|
|
55
|
+
const isSuccess = status === 'success'
|
|
56
|
+
const isError = status === 'error'
|
|
57
|
+
|
|
58
|
+
const defaultStatusText = isRunning ? runningText : isSuccess ? ranText : failedText
|
|
59
|
+
const statusText = statusTextProp !== undefined ? statusTextProp : defaultStatusText
|
|
60
|
+
const showStatusText = statusText !== ''
|
|
61
|
+
|
|
62
|
+
const defaultTime = timeIcon ?? <TimeLine className={timeClass} />
|
|
63
|
+
const defaultCheck = checkboxCircleIcon ?? <CheckboxCircleLine className={checkboxClass} />
|
|
64
|
+
const defaultCloseCircle = closeCircleIcon ?? <CloseCircleLine className={closeCircleClass} />
|
|
65
|
+
const defaultArrow = arrowRightIcon ?? <ArrowRightLine className={arrowClass} />
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={cn('flex flex-col', className)}>
|
|
69
|
+
<div
|
|
70
|
+
onClick={() => hasResponse && !isRunning && setIsExpanded(!isExpanded)}
|
|
71
|
+
className={cn(
|
|
72
|
+
'group flex items-center gap-1.5 min-h-5',
|
|
73
|
+
hasResponse && !isRunning && 'cursor-pointer hover:opacity-80 transition-opacity'
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<div className="shrink-0 flex items-center justify-center text-xs">
|
|
77
|
+
{isRunning && defaultTime}
|
|
78
|
+
{isSuccess && defaultCheck}
|
|
79
|
+
{isError && defaultCloseCircle}
|
|
80
|
+
</div>
|
|
81
|
+
{showStatusText && (
|
|
82
|
+
<span className="text-xs leading-xs text-text-tertiary shrink-0">{statusText}</span>
|
|
83
|
+
)}
|
|
84
|
+
<span className="text-xs leading-xs text-text-secondary shrink-0">{toolName}</span>
|
|
85
|
+
{params != null && params !== '' && (
|
|
86
|
+
<span
|
|
87
|
+
className="text-xs leading-xs text-text-tertiary min-w-0 shrink truncate"
|
|
88
|
+
title={params}
|
|
89
|
+
>
|
|
90
|
+
{params}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
{hasResponse && !isRunning && (
|
|
94
|
+
<div
|
|
95
|
+
className={cn(
|
|
96
|
+
'shrink-0 inline-flex items-center justify-center',
|
|
97
|
+
'opacity-0 group-hover:opacity-100',
|
|
98
|
+
isExpanded && '!opacity-100',
|
|
99
|
+
isExpanded ? 'rotate-90' : 'rotate-0'
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{defaultArrow}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{hasResponse && isExpanded && (
|
|
108
|
+
<div className="mt-1 ml-4.5 rounded-md border border-border-tertiary bg-bg-base overflow-hidden font-mono text-xs leading-xs">
|
|
109
|
+
<div className="text-text-tertiary px-2 pt-1 pb-0 leading-xs">
|
|
110
|
+
{responseLabel}
|
|
111
|
+
</div>
|
|
112
|
+
<Scrollbar
|
|
113
|
+
scrollbarVisibility="auto"
|
|
114
|
+
maxHeight={208}
|
|
115
|
+
className="px-2 pb-1 leading-xs"
|
|
116
|
+
>
|
|
117
|
+
<pre
|
|
118
|
+
className={cn(
|
|
119
|
+
'whitespace-pre-wrap break-words leading-xs',
|
|
120
|
+
isError ? 'text-error' : 'text-text-tertiary'
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{response}
|
|
124
|
+
</pre>
|
|
125
|
+
</Scrollbar>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
ToolInvocationCard.displayName = 'ToolInvocationCard'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
export interface UserMessageProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
6
|
+
children: ReactNode
|
|
7
|
+
maxWidth?: string | number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const UserMessage = forwardRef<HTMLDivElement, UserMessageProps>(
|
|
11
|
+
({ children, className, maxWidth = '752px', ...props }, ref) => {
|
|
12
|
+
const containerStyles = cn(
|
|
13
|
+
'w-full',
|
|
14
|
+
'bg-sage-bg',
|
|
15
|
+
'rounded-lg',
|
|
16
|
+
'p-3',
|
|
17
|
+
'text-sm leading-sm',
|
|
18
|
+
'font-normal',
|
|
19
|
+
'text-text',
|
|
20
|
+
className
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={containerStyles}
|
|
27
|
+
style={{
|
|
28
|
+
maxWidth: typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth,
|
|
29
|
+
}}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
UserMessage.displayName = 'UserMessage'
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { OptionList } from '../../basic/option-list'
|
|
5
|
+
import type { OptionItem } from '../../basic/option-list'
|
|
6
|
+
import type { UserQuestionItem, UserQuestionLabels } from './types'
|
|
7
|
+
|
|
8
|
+
export interface UserQuestionCardProps {
|
|
9
|
+
question: UserQuestionItem
|
|
10
|
+
qIndex: number
|
|
11
|
+
labels: Required<UserQuestionLabels>
|
|
12
|
+
answers: Record<string, string[]>
|
|
13
|
+
customInputs: Record<string, string>
|
|
14
|
+
focusedOptionIndex: number
|
|
15
|
+
isCurrentQuestion: boolean
|
|
16
|
+
isSubmitting: boolean
|
|
17
|
+
allQuestionsAnswered: boolean
|
|
18
|
+
questionsLength: number
|
|
19
|
+
questionRefs: MutableRefObject<Map<number, HTMLDivElement>>
|
|
20
|
+
customInputRefs: MutableRefObject<Map<string, HTMLInputElement>>
|
|
21
|
+
setCurrentQuestionIndex: (i: number) => void
|
|
22
|
+
setFocusedOptionIndex: (i: number) => void
|
|
23
|
+
setAnswers: Dispatch<SetStateAction<Record<string, string[]>>>
|
|
24
|
+
setCustomInputs: Dispatch<SetStateAction<Record<string, string>>>
|
|
25
|
+
getSelectedOptionIndex: (i: number) => number
|
|
26
|
+
shouldScrollToQuestionRef: MutableRefObject<boolean>
|
|
27
|
+
onOptionClick: (questionText: string, optionLabel: string, questionIndex: number) => void
|
|
28
|
+
onContinue: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function UserQuestionCard({
|
|
32
|
+
question,
|
|
33
|
+
qIndex,
|
|
34
|
+
labels,
|
|
35
|
+
answers,
|
|
36
|
+
customInputs,
|
|
37
|
+
focusedOptionIndex,
|
|
38
|
+
isCurrentQuestion,
|
|
39
|
+
isSubmitting,
|
|
40
|
+
allQuestionsAnswered,
|
|
41
|
+
questionsLength,
|
|
42
|
+
questionRefs,
|
|
43
|
+
customInputRefs,
|
|
44
|
+
setCurrentQuestionIndex,
|
|
45
|
+
setFocusedOptionIndex,
|
|
46
|
+
setAnswers,
|
|
47
|
+
setCustomInputs,
|
|
48
|
+
getSelectedOptionIndex,
|
|
49
|
+
shouldScrollToQuestionRef,
|
|
50
|
+
onOptionClick,
|
|
51
|
+
onContinue,
|
|
52
|
+
}: UserQuestionCardProps) {
|
|
53
|
+
const questionOptions = question.options || []
|
|
54
|
+
const [isInputFocused, setIsInputFocused] = useState(false)
|
|
55
|
+
const hasInputValue = !!(customInputs[question.question]?.trim())
|
|
56
|
+
const showUnderline = isInputFocused || hasInputValue
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
key={question.question}
|
|
61
|
+
ref={(el) => {
|
|
62
|
+
if (el) questionRefs.current.set(qIndex, el)
|
|
63
|
+
else questionRefs.current.delete(qIndex)
|
|
64
|
+
}}
|
|
65
|
+
className={cn(
|
|
66
|
+
'transition-opacity duration-200',
|
|
67
|
+
isCurrentQuestion ? 'opacity-100' : 'opacity-40'
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
<div className="flex items-start gap-3 pl-2 mb-3">
|
|
71
|
+
<div className="flex-shrink-0 w-5 flex items-center justify-center text-sm leading-sm font-medium text-text-tertiary">
|
|
72
|
+
{qIndex + 1}.
|
|
73
|
+
</div>
|
|
74
|
+
<div className="text-sm leading-sm font-medium text-text min-w-0 flex-1">
|
|
75
|
+
{question.question}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<OptionList
|
|
79
|
+
items={questionOptions.map((opt) => ({
|
|
80
|
+
id: opt.label,
|
|
81
|
+
label: opt.label,
|
|
82
|
+
description: opt.description,
|
|
83
|
+
}))}
|
|
84
|
+
selectedIds={answers[question.question]?.map((l) => l) || []}
|
|
85
|
+
focusedId={
|
|
86
|
+
isCurrentQuestion ? questionOptions[focusedOptionIndex]?.label : undefined
|
|
87
|
+
}
|
|
88
|
+
disabled={isSubmitting || !isCurrentQuestion}
|
|
89
|
+
onItemClick={(item: OptionItem, index: number) => {
|
|
90
|
+
if (!isCurrentQuestion) {
|
|
91
|
+
setCurrentQuestionIndex(qIndex)
|
|
92
|
+
setFocusedOptionIndex(getSelectedOptionIndex(qIndex))
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
onOptionClick(question.question, item.label, qIndex)
|
|
96
|
+
setFocusedOptionIndex(index)
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
{question.allowCustomInput !== false && (
|
|
100
|
+
<div
|
|
101
|
+
className={cn(
|
|
102
|
+
'w-full flex gap-3 pl-2 mt-3 transition-colors',
|
|
103
|
+
!isCurrentQuestion && 'opacity-40',
|
|
104
|
+
isSubmitting && 'opacity-50'
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
<div
|
|
108
|
+
className={cn(
|
|
109
|
+
'flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-xs leading-xs font-medium transition-colors',
|
|
110
|
+
(isCurrentQuestion && focusedOptionIndex === questionOptions.length) ||
|
|
111
|
+
customInputs[question.question]?.trim()
|
|
112
|
+
? 'bg-primary-active text-text-on-primary'
|
|
113
|
+
: 'bg-fill-secondary text-text-tertiary'
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
{questionOptions.length + 1}
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex-1 min-w-0 flex flex-col">
|
|
119
|
+
<div className="h-5 flex items-center">
|
|
120
|
+
<input
|
|
121
|
+
ref={(el) => {
|
|
122
|
+
if (el) customInputRefs.current.set(question.question, el)
|
|
123
|
+
else customInputRefs.current.delete(question.question)
|
|
124
|
+
}}
|
|
125
|
+
type="text"
|
|
126
|
+
value={customInputs[question.question] || ''}
|
|
127
|
+
style={{ outline: 'none', outlineOffset: 0, boxShadow: 'none', border: 'none' }}
|
|
128
|
+
onChange={(e) => {
|
|
129
|
+
const value = e.target.value
|
|
130
|
+
setCustomInputs((prev) => ({ ...prev, [question.question]: value }))
|
|
131
|
+
if (!question.multiSelect && value.trim()) {
|
|
132
|
+
setAnswers((prev) => ({
|
|
133
|
+
...prev,
|
|
134
|
+
[question.question]: [],
|
|
135
|
+
}))
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
onFocus={() => {
|
|
139
|
+
setIsInputFocused(true)
|
|
140
|
+
if (isCurrentQuestion) {
|
|
141
|
+
setFocusedOptionIndex(questionOptions.length)
|
|
142
|
+
if (!question.multiSelect) {
|
|
143
|
+
setAnswers((prev) => ({
|
|
144
|
+
...prev,
|
|
145
|
+
[question.question]: [],
|
|
146
|
+
}))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}}
|
|
150
|
+
onBlur={() => setIsInputFocused(false)}
|
|
151
|
+
onKeyDown={(e) => {
|
|
152
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
153
|
+
e.preventDefault()
|
|
154
|
+
e.stopPropagation()
|
|
155
|
+
const currentCustomInput = customInputs[question.question]?.trim()
|
|
156
|
+
const currentSelectedOptions = answers[question.question] || []
|
|
157
|
+
const hasAnswer =
|
|
158
|
+
currentSelectedOptions.length > 0 || !!currentCustomInput
|
|
159
|
+
if (hasAnswer) {
|
|
160
|
+
if (allQuestionsAnswered) {
|
|
161
|
+
onContinue()
|
|
162
|
+
} else if (qIndex < questionsLength - 1) {
|
|
163
|
+
const nextIndex = qIndex + 1
|
|
164
|
+
shouldScrollToQuestionRef.current = true
|
|
165
|
+
setCurrentQuestionIndex(nextIndex)
|
|
166
|
+
setFocusedOptionIndex(0)
|
|
167
|
+
} else {
|
|
168
|
+
onContinue()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
placeholder={labels.customInputPlaceholder}
|
|
174
|
+
disabled={isSubmitting || !isCurrentQuestion}
|
|
175
|
+
className={cn(
|
|
176
|
+
'w-full h-full py-0 bg-transparent text-sm leading-sm font-medium text-text',
|
|
177
|
+
'placeholder:text-text-tertiary placeholder:font-normal',
|
|
178
|
+
'border-0 rounded-none outline-none outline-offset-0',
|
|
179
|
+
'focus:outline-none focus:outline-offset-0',
|
|
180
|
+
'focus-visible:outline-none focus-visible:outline-offset-0',
|
|
181
|
+
'ring-0 focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none',
|
|
182
|
+
'disabled:cursor-not-allowed'
|
|
183
|
+
)}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
<div
|
|
187
|
+
className="mt-1.5 overflow-hidden origin-left transition-[width] duration-200 ease-out"
|
|
188
|
+
style={{ width: showUnderline ? '100%' : '0%' }}
|
|
189
|
+
aria-hidden
|
|
190
|
+
>
|
|
191
|
+
<div className="h-px bg-[var(--color-border-tertiary)]" />
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { Button } from '../../basic/button'
|
|
3
|
+
import type { UserQuestionLabels } from './types'
|
|
4
|
+
|
|
5
|
+
export interface UserQuestionFooterProps {
|
|
6
|
+
labels: Required<UserQuestionLabels>
|
|
7
|
+
sparklingIcon: ReactNode
|
|
8
|
+
isSubmitting: boolean
|
|
9
|
+
hasCustomText: boolean
|
|
10
|
+
allQuestionsAnswered: boolean
|
|
11
|
+
onRecommend: () => void
|
|
12
|
+
onSkip: () => void
|
|
13
|
+
onContinue: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function UserQuestionFooter({
|
|
17
|
+
labels,
|
|
18
|
+
sparklingIcon,
|
|
19
|
+
isSubmitting,
|
|
20
|
+
hasCustomText,
|
|
21
|
+
allQuestionsAnswered,
|
|
22
|
+
onRecommend,
|
|
23
|
+
onSkip,
|
|
24
|
+
onContinue,
|
|
25
|
+
}: UserQuestionFooterProps) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex items-center justify-between px-3 py-2">
|
|
28
|
+
<Button
|
|
29
|
+
variant="ghost"
|
|
30
|
+
size="sm"
|
|
31
|
+
onClick={onRecommend}
|
|
32
|
+
disabled={isSubmitting}
|
|
33
|
+
>
|
|
34
|
+
{sparklingIcon}
|
|
35
|
+
{labels.recommend}
|
|
36
|
+
</Button>
|
|
37
|
+
<div className="flex items-center gap-2">
|
|
38
|
+
{!allQuestionsAnswered && (
|
|
39
|
+
<Button
|
|
40
|
+
variant="ghost"
|
|
41
|
+
size="sm"
|
|
42
|
+
onClick={onSkip}
|
|
43
|
+
disabled={isSubmitting}
|
|
44
|
+
>
|
|
45
|
+
{labels.skipAll}
|
|
46
|
+
</Button>
|
|
47
|
+
)}
|
|
48
|
+
<Button
|
|
49
|
+
size="sm"
|
|
50
|
+
variant="secondary"
|
|
51
|
+
onClick={onContinue}
|
|
52
|
+
disabled={isSubmitting || hasCustomText || !allQuestionsAnswered}
|
|
53
|
+
>
|
|
54
|
+
{isSubmitting ? (
|
|
55
|
+
labels.sending
|
|
56
|
+
) : (
|
|
57
|
+
<>
|
|
58
|
+
{allQuestionsAnswered ? labels.submit : labels.continue}
|
|
59
|
+
<span className="opacity-70"> ↵</span>
|
|
60
|
+
</>
|
|
61
|
+
)}
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { IconButton } from '../../basic/icon-button'
|
|
3
|
+
import type { UserQuestionItem } from './types'
|
|
4
|
+
import type { UserQuestionLabels } from './types'
|
|
5
|
+
|
|
6
|
+
export interface UserQuestionHeaderProps {
|
|
7
|
+
currentQuestion: UserQuestionItem | undefined
|
|
8
|
+
questionsLength: number
|
|
9
|
+
labels: Required<UserQuestionLabels>
|
|
10
|
+
chat4Icon: ReactNode
|
|
11
|
+
arrowUpSIcon: ReactNode
|
|
12
|
+
arrowDownSIcon: ReactNode
|
|
13
|
+
currentQuestionIndex: number
|
|
14
|
+
onPrevious: () => void
|
|
15
|
+
onNext: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function UserQuestionHeader({
|
|
19
|
+
currentQuestion,
|
|
20
|
+
questionsLength,
|
|
21
|
+
labels,
|
|
22
|
+
chat4Icon,
|
|
23
|
+
arrowUpSIcon,
|
|
24
|
+
arrowDownSIcon,
|
|
25
|
+
currentQuestionIndex,
|
|
26
|
+
onPrevious,
|
|
27
|
+
onNext,
|
|
28
|
+
}: UserQuestionHeaderProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex items-center justify-between mb-2 -mt-2 px-3 pt-2 pb-2 rounded-t-xl bg-fill-tertiary">
|
|
31
|
+
<div className="flex items-center gap-1.5">
|
|
32
|
+
{chat4Icon}
|
|
33
|
+
<span className="text-xs leading-xs text-text-tertiary">
|
|
34
|
+
{currentQuestion?.header || labels.questionHeader}
|
|
35
|
+
</span>
|
|
36
|
+
<span className="text-text-tertiary/50">•</span>
|
|
37
|
+
<span className="text-xs leading-xs text-text-tertiary">
|
|
38
|
+
{currentQuestion?.multiSelect ? labels.multiSelect : labels.singleSelect}
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
{questionsLength > 1 && (
|
|
42
|
+
<div className="flex items-center gap-1">
|
|
43
|
+
<IconButton
|
|
44
|
+
size="sm"
|
|
45
|
+
variant="ghost"
|
|
46
|
+
onClick={onPrevious}
|
|
47
|
+
disabled={currentQuestionIndex === 0}
|
|
48
|
+
icon={arrowUpSIcon}
|
|
49
|
+
/>
|
|
50
|
+
<span className="text-xs leading-xs text-text-tertiary px-1">
|
|
51
|
+
{currentQuestionIndex + 1} / {questionsLength}
|
|
52
|
+
</span>
|
|
53
|
+
<IconButton
|
|
54
|
+
size="sm"
|
|
55
|
+
variant="ghost"
|
|
56
|
+
onClick={onNext}
|
|
57
|
+
disabled={currentQuestionIndex === questionsLength - 1}
|
|
58
|
+
icon={arrowDownSIcon}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|