@standardagents/cli 0.10.1-next.bbd142a → 0.11.0-next.ab7e1ea
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/LICENSE.txt +48 -0
- package/chat/next/app/layout.tsx +24 -0
- package/chat/next/app/page.tsx +21 -0
- package/chat/next/next-env.d.ts +6 -0
- package/chat/next/next.config.ts +15 -0
- package/chat/next/postcss.config.mjs +5 -0
- package/chat/next/tsconfig.json +27 -0
- package/chat/package.json +32 -0
- package/chat/src/App.tsx +130 -0
- package/chat/src/components/AgentSelector.tsx +102 -0
- package/chat/src/components/Chat.tsx +134 -0
- package/chat/src/components/EmptyState.tsx +437 -0
- package/chat/src/components/Login.tsx +139 -0
- package/chat/src/components/Logo.tsx +39 -0
- package/chat/src/components/Markdown.tsx +222 -0
- package/chat/src/components/MessageInput.tsx +197 -0
- package/chat/src/components/MessageList.tsx +796 -0
- package/chat/src/components/Sidebar.tsx +253 -0
- package/chat/src/hooks/useAuth.tsx +118 -0
- package/chat/src/hooks/useTheme.tsx +55 -0
- package/chat/src/hooks/useThreads.ts +131 -0
- package/chat/src/index.css +168 -0
- package/chat/tsconfig.json +24 -0
- package/chat/vite/favicon.svg +3 -0
- package/chat/vite/index.html +17 -0
- package/chat/vite/main.tsx +25 -0
- package/chat/vite/vite.config.ts +23 -0
- package/dist/index.js +669 -99
- package/dist/index.js.map +1 -1
- package/package.json +13 -9
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react'
|
|
4
|
+
import ReactMarkdown from 'react-markdown'
|
|
5
|
+
import ShikiHighlighter, { isInlineCode } from 'react-shiki'
|
|
6
|
+
import type { Components } from 'react-markdown'
|
|
7
|
+
import type { Element } from 'hast'
|
|
8
|
+
|
|
9
|
+
// Copy icon component
|
|
10
|
+
function CopyIcon({ className }: { className?: string }) {
|
|
11
|
+
return (
|
|
12
|
+
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
13
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
14
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
15
|
+
</svg>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function CheckIcon({ className }: { className?: string }) {
|
|
20
|
+
return (
|
|
21
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
22
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
23
|
+
</svg>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Language aliases for common variations
|
|
28
|
+
const langAlias: Record<string, string> = {
|
|
29
|
+
js: 'javascript',
|
|
30
|
+
ts: 'typescript',
|
|
31
|
+
tsx: 'tsx',
|
|
32
|
+
jsx: 'jsx',
|
|
33
|
+
sh: 'bash',
|
|
34
|
+
shell: 'bash',
|
|
35
|
+
zsh: 'bash',
|
|
36
|
+
yml: 'yaml',
|
|
37
|
+
py: 'python',
|
|
38
|
+
rb: 'ruby',
|
|
39
|
+
rs: 'rust',
|
|
40
|
+
go: 'go',
|
|
41
|
+
md: 'markdown',
|
|
42
|
+
mdx: 'mdx',
|
|
43
|
+
vue: 'vue',
|
|
44
|
+
svelte: 'svelte',
|
|
45
|
+
dockerfile: 'docker',
|
|
46
|
+
tf: 'hcl',
|
|
47
|
+
terraform: 'hcl',
|
|
48
|
+
cs: 'csharp',
|
|
49
|
+
'c#': 'csharp',
|
|
50
|
+
'c++': 'cpp',
|
|
51
|
+
kt: 'kotlin',
|
|
52
|
+
objc: 'objective-c',
|
|
53
|
+
'objective-c': 'objc',
|
|
54
|
+
pl: 'perl',
|
|
55
|
+
hs: 'haskell',
|
|
56
|
+
ex: 'elixir',
|
|
57
|
+
erl: 'erlang',
|
|
58
|
+
fs: 'fsharp',
|
|
59
|
+
'f#': 'fsharp',
|
|
60
|
+
ps1: 'powershell',
|
|
61
|
+
psm1: 'powershell',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface CodeBlockProps {
|
|
65
|
+
className?: string
|
|
66
|
+
children?: React.ReactNode
|
|
67
|
+
node?: Element
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function CodeBlock({ className, children, node, ...props }: CodeBlockProps) {
|
|
71
|
+
const [copied, setCopied] = useState(false)
|
|
72
|
+
const code = String(children).trim()
|
|
73
|
+
const match = className?.match(/language-(\w+)/)
|
|
74
|
+
let language = match ? match[1] : 'text'
|
|
75
|
+
|
|
76
|
+
// Apply language aliases
|
|
77
|
+
if (language in langAlias) {
|
|
78
|
+
language = langAlias[language]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isInline = node ? isInlineCode(node) : !code.includes('\n')
|
|
82
|
+
|
|
83
|
+
const handleCopy = useCallback(async () => {
|
|
84
|
+
await navigator.clipboard.writeText(code)
|
|
85
|
+
setCopied(true)
|
|
86
|
+
setTimeout(() => setCopied(false), 2000)
|
|
87
|
+
}, [code])
|
|
88
|
+
|
|
89
|
+
if (isInline) {
|
|
90
|
+
return (
|
|
91
|
+
<code
|
|
92
|
+
className="px-1.5 py-0.5 rounded bg-[var(--code-bg)] text-[var(--text-primary)] text-sm font-mono"
|
|
93
|
+
{...props}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</code>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className="relative group my-4 rounded-lg overflow-hidden bg-[var(--code-bg)]">
|
|
102
|
+
<button
|
|
103
|
+
onClick={handleCopy}
|
|
104
|
+
className="absolute top-2 right-2 px-2 py-1 rounded-md flex items-center gap-1 text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] transition-all z-10"
|
|
105
|
+
title={copied ? 'Copied!' : 'Copy code'}
|
|
106
|
+
>
|
|
107
|
+
{copied ? (
|
|
108
|
+
<>
|
|
109
|
+
<CheckIcon className="w-3 h-3 text-emerald-500" />
|
|
110
|
+
<span className="text-emerald-500">Copied</span>
|
|
111
|
+
</>
|
|
112
|
+
) : (
|
|
113
|
+
<>
|
|
114
|
+
<CopyIcon className="w-3 h-3" />
|
|
115
|
+
<span>Copy</span>
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
</button>
|
|
119
|
+
<ShikiHighlighter
|
|
120
|
+
language={language}
|
|
121
|
+
theme={{
|
|
122
|
+
light: 'github-light',
|
|
123
|
+
dark: 'github-dark',
|
|
124
|
+
}}
|
|
125
|
+
defaultColor="light-dark()"
|
|
126
|
+
langAlias={langAlias}
|
|
127
|
+
showLanguage={false}
|
|
128
|
+
addDefaultStyles={false}
|
|
129
|
+
className="text-sm p-4"
|
|
130
|
+
{...props}
|
|
131
|
+
>
|
|
132
|
+
{code}
|
|
133
|
+
</ShikiHighlighter>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Custom components for react-markdown
|
|
139
|
+
const components: Components = {
|
|
140
|
+
code: CodeBlock as Components['code'],
|
|
141
|
+
// Style other markdown elements
|
|
142
|
+
p: ({ children }) => (
|
|
143
|
+
<p className="mb-4 last:mb-0">{children}</p>
|
|
144
|
+
),
|
|
145
|
+
ul: ({ children }) => (
|
|
146
|
+
<ul className="list-disc list-inside mb-4 space-y-1">{children}</ul>
|
|
147
|
+
),
|
|
148
|
+
ol: ({ children }) => (
|
|
149
|
+
<ol className="list-decimal list-inside mb-4 space-y-1">{children}</ol>
|
|
150
|
+
),
|
|
151
|
+
li: ({ children }) => (
|
|
152
|
+
<li className="text-[var(--text-primary)]">{children}</li>
|
|
153
|
+
),
|
|
154
|
+
h1: ({ children }) => (
|
|
155
|
+
<h1 className="text-2xl font-bold mb-4 mt-6 first:mt-0 text-[var(--text-primary)]">{children}</h1>
|
|
156
|
+
),
|
|
157
|
+
h2: ({ children }) => (
|
|
158
|
+
<h2 className="text-xl font-bold mb-3 mt-5 first:mt-0 text-[var(--text-primary)]">{children}</h2>
|
|
159
|
+
),
|
|
160
|
+
h3: ({ children }) => (
|
|
161
|
+
<h3 className="text-lg font-bold mb-2 mt-4 first:mt-0 text-[var(--text-primary)]">{children}</h3>
|
|
162
|
+
),
|
|
163
|
+
h4: ({ children }) => (
|
|
164
|
+
<h4 className="text-base font-bold mb-2 mt-3 first:mt-0 text-[var(--text-primary)]">{children}</h4>
|
|
165
|
+
),
|
|
166
|
+
blockquote: ({ children }) => (
|
|
167
|
+
<blockquote className="border-l-4 border-[var(--border-secondary)] pl-4 my-4 text-[var(--text-secondary)] italic">
|
|
168
|
+
{children}
|
|
169
|
+
</blockquote>
|
|
170
|
+
),
|
|
171
|
+
a: ({ href, children }) => (
|
|
172
|
+
<a
|
|
173
|
+
href={href}
|
|
174
|
+
target="_blank"
|
|
175
|
+
rel="noopener noreferrer"
|
|
176
|
+
className="text-blue-500 hover:text-blue-400 underline"
|
|
177
|
+
>
|
|
178
|
+
{children}
|
|
179
|
+
</a>
|
|
180
|
+
),
|
|
181
|
+
strong: ({ children }) => (
|
|
182
|
+
<strong className="font-semibold text-[var(--text-primary)]">{children}</strong>
|
|
183
|
+
),
|
|
184
|
+
em: ({ children }) => (
|
|
185
|
+
<em className="italic">{children}</em>
|
|
186
|
+
),
|
|
187
|
+
hr: () => (
|
|
188
|
+
<hr className="my-6 border-[var(--border-primary)]" />
|
|
189
|
+
),
|
|
190
|
+
table: ({ children }) => (
|
|
191
|
+
<div className="overflow-x-auto my-4">
|
|
192
|
+
<table className="min-w-full border-collapse border border-[var(--border-primary)]">
|
|
193
|
+
{children}
|
|
194
|
+
</table>
|
|
195
|
+
</div>
|
|
196
|
+
),
|
|
197
|
+
th: ({ children }) => (
|
|
198
|
+
<th className="border border-[var(--border-primary)] px-3 py-2 bg-[var(--bg-tertiary)] text-left font-semibold">
|
|
199
|
+
{children}
|
|
200
|
+
</th>
|
|
201
|
+
),
|
|
202
|
+
td: ({ children }) => (
|
|
203
|
+
<td className="border border-[var(--border-primary)] px-3 py-2">
|
|
204
|
+
{children}
|
|
205
|
+
</td>
|
|
206
|
+
),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface MarkdownProps {
|
|
210
|
+
children: string
|
|
211
|
+
className?: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function Markdown({ children, className }: MarkdownProps) {
|
|
215
|
+
return (
|
|
216
|
+
<div className={className}>
|
|
217
|
+
<ReactMarkdown components={components}>
|
|
218
|
+
{children}
|
|
219
|
+
</ReactMarkdown>
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
4
|
+
import { useThread } from '@standardagents/react'
|
|
5
|
+
|
|
6
|
+
export function MessageInput() {
|
|
7
|
+
const [input, setInput] = useState('')
|
|
8
|
+
const [sending, setSending] = useState(false)
|
|
9
|
+
const {
|
|
10
|
+
loading,
|
|
11
|
+
sendMessage,
|
|
12
|
+
stopExecution,
|
|
13
|
+
addAttachment,
|
|
14
|
+
attachments,
|
|
15
|
+
removeAttachment,
|
|
16
|
+
getPreviewUrl,
|
|
17
|
+
} = useThread()
|
|
18
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
19
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
20
|
+
|
|
21
|
+
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
|
22
|
+
e.preventDefault()
|
|
23
|
+
if ((!input.trim() && attachments.length === 0) || loading || sending) return
|
|
24
|
+
|
|
25
|
+
const message = input
|
|
26
|
+
setInput('')
|
|
27
|
+
setSending(true)
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// SDK's sendMessage automatically includes queued attachments and clears them
|
|
31
|
+
await sendMessage({ role: 'user', content: message })
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Failed to send message:', error)
|
|
34
|
+
setInput(message)
|
|
35
|
+
} finally {
|
|
36
|
+
setSending(false)
|
|
37
|
+
}
|
|
38
|
+
}, [input, attachments.length, loading, sending, sendMessage])
|
|
39
|
+
|
|
40
|
+
const handleFileSelect = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
41
|
+
const files = e.target.files
|
|
42
|
+
if (!files || files.length === 0) return
|
|
43
|
+
|
|
44
|
+
// SDK handles base64 encoding, dimensions, and preview URLs
|
|
45
|
+
for (const file of Array.from(files)) {
|
|
46
|
+
await addAttachment(file)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Reset file input and refocus textarea
|
|
50
|
+
if (fileInputRef.current) {
|
|
51
|
+
fileInputRef.current.value = ''
|
|
52
|
+
}
|
|
53
|
+
textareaRef.current?.focus()
|
|
54
|
+
}, [addAttachment])
|
|
55
|
+
|
|
56
|
+
// Auto-resize textarea
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const textarea = textareaRef.current
|
|
59
|
+
if (textarea) {
|
|
60
|
+
textarea.style.height = 'auto'
|
|
61
|
+
const newHeight = Math.min(Math.max(textarea.scrollHeight, 24), 200)
|
|
62
|
+
textarea.style.height = newHeight + 'px'
|
|
63
|
+
}
|
|
64
|
+
}, [input])
|
|
65
|
+
|
|
66
|
+
// Focus textarea when not loading
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!loading && !sending) {
|
|
69
|
+
textareaRef.current?.focus()
|
|
70
|
+
}
|
|
71
|
+
}, [loading, sending])
|
|
72
|
+
|
|
73
|
+
const isDisabled = loading || sending
|
|
74
|
+
const canSend = (input.trim() || attachments.length > 0) && !isDisabled
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="bg-[var(--bg-tertiary)]/80 backdrop-blur-md rounded-2xl border border-[var(--border-primary)] shadow-lg shadow-black/10 overflow-hidden">
|
|
78
|
+
{/* Attachment drawer - extends from top of input */}
|
|
79
|
+
{attachments.length > 0 && (
|
|
80
|
+
<div className="px-3 pt-3 pb-2 border-b border-[var(--border-secondary)]">
|
|
81
|
+
<div className="flex flex-wrap gap-2">
|
|
82
|
+
{attachments.map((attachment) => {
|
|
83
|
+
// Try multiple ways to get preview URL
|
|
84
|
+
const previewUrl = getPreviewUrl(attachment) || (attachment as any).localPreviewUrl || (attachment as any).previewUrl
|
|
85
|
+
const isImage = attachment.mimeType?.startsWith('image/') || (attachment as any).type?.startsWith('image/')
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
key={attachment.id}
|
|
89
|
+
className="relative group"
|
|
90
|
+
>
|
|
91
|
+
{isImage && previewUrl ? (
|
|
92
|
+
<img
|
|
93
|
+
src={previewUrl}
|
|
94
|
+
alt={attachment.name}
|
|
95
|
+
className="h-16 w-auto rounded-lg object-cover border border-[var(--border-secondary)]"
|
|
96
|
+
/>
|
|
97
|
+
) : (
|
|
98
|
+
<div className="h-16 px-3 flex items-center gap-2 rounded-lg bg-[var(--bg-hover)] border border-[var(--border-secondary)]">
|
|
99
|
+
<svg className="w-4 h-4 text-[var(--text-secondary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
100
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
101
|
+
</svg>
|
|
102
|
+
<span className="text-xs text-[var(--text-secondary)] max-w-[100px] truncate">{attachment.name}</span>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={() => removeAttachment(attachment.id)}
|
|
108
|
+
className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-[var(--bg-active)] hover:bg-[var(--bg-hover)] rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
|
109
|
+
>
|
|
110
|
+
<svg className="w-3 h-3 text-[var(--text-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
111
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
112
|
+
</svg>
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
})}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{/* Input row */}
|
|
122
|
+
<form onSubmit={handleSubmit}>
|
|
123
|
+
<div className="flex items-center gap-2 px-2 py-1.5">
|
|
124
|
+
{/* Attachment button */}
|
|
125
|
+
<input
|
|
126
|
+
ref={fileInputRef}
|
|
127
|
+
type="file"
|
|
128
|
+
multiple
|
|
129
|
+
accept="image/*,.pdf,.txt,.md,.json,.csv"
|
|
130
|
+
onChange={handleFileSelect}
|
|
131
|
+
className="hidden"
|
|
132
|
+
/>
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={() => fileInputRef.current?.click()}
|
|
136
|
+
disabled={isDisabled}
|
|
137
|
+
className="p-2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
|
|
138
|
+
title="Attach files"
|
|
139
|
+
>
|
|
140
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
141
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13" />
|
|
142
|
+
</svg>
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
{/* Textarea */}
|
|
146
|
+
<textarea
|
|
147
|
+
ref={textareaRef}
|
|
148
|
+
value={input}
|
|
149
|
+
onChange={(e) => setInput(e.target.value)}
|
|
150
|
+
onKeyDown={(e) => {
|
|
151
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
152
|
+
e.preventDefault()
|
|
153
|
+
handleSubmit(e)
|
|
154
|
+
}
|
|
155
|
+
}}
|
|
156
|
+
placeholder="Message..."
|
|
157
|
+
rows={1}
|
|
158
|
+
disabled={isDisabled}
|
|
159
|
+
className="flex-1 bg-transparent resize-none text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none disabled:opacity-50 text-[15px] leading-normal py-2.5 max-h-[200px] overflow-y-auto scrollbar-hide"
|
|
160
|
+
style={{ scrollbarWidth: 'none' }}
|
|
161
|
+
/>
|
|
162
|
+
|
|
163
|
+
{/* Stop / Send button */}
|
|
164
|
+
{loading ? (
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
onClick={stopExecution}
|
|
168
|
+
className="p-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-xl transition-colors shrink-0"
|
|
169
|
+
title="Stop generating"
|
|
170
|
+
>
|
|
171
|
+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
172
|
+
<rect x="6" y="6" width="12" height="12" rx="2" />
|
|
173
|
+
</svg>
|
|
174
|
+
</button>
|
|
175
|
+
) : (
|
|
176
|
+
<button
|
|
177
|
+
type="submit"
|
|
178
|
+
disabled={!canSend}
|
|
179
|
+
className="p-2 rounded-xl bg-[var(--text-primary)] text-[var(--bg-primary)] hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150 shrink-0"
|
|
180
|
+
>
|
|
181
|
+
{sending ? (
|
|
182
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
183
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
184
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
185
|
+
</svg>
|
|
186
|
+
) : (
|
|
187
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
188
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" />
|
|
189
|
+
</svg>
|
|
190
|
+
)}
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
</form>
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|