@aprovan/patchwork-chat 0.1.0-dev.6bd527d
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/.turbo/turbo-build.log +22 -0
- package/.utcp_config.json +14 -0
- package/.working/widgets/27060b91-a2a5-4272-b243-6eb904bd4070/main.tsx +107 -0
- package/LICENSE +373 -0
- package/dist/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/dist/assets/index-Ct0GSTdJ.css +1 -0
- package/dist/assets/index-ueH8ysw1.js +1455 -0
- package/dist/index.html +18 -0
- package/index.html +17 -0
- package/package.json +55 -0
- package/postcss.config.js +6 -0
- package/src/App.tsx +7 -0
- package/src/components/ui/avatar.tsx +48 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +86 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/dialog.tsx +60 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/scroll-area.tsx +46 -0
- package/src/index.css +190 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/pages/ChatPage.tsx +461 -0
- package/tailwind.config.js +71 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +26 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { useChat } from '@ai-sdk/react';
|
|
2
|
+
import { DefaultChatTransport } from 'ai';
|
|
3
|
+
import { useState, useRef, useEffect, useMemo, useCallback, createContext, useContext } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Send,
|
|
6
|
+
Loader2,
|
|
7
|
+
Wrench,
|
|
8
|
+
AlertCircle,
|
|
9
|
+
Brain,
|
|
10
|
+
ChevronDown,
|
|
11
|
+
} from 'lucide-react';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
14
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
15
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
16
|
+
import { Badge } from '@/components/ui/badge';
|
|
17
|
+
import {
|
|
18
|
+
Collapsible,
|
|
19
|
+
CollapsibleContent,
|
|
20
|
+
CollapsibleTrigger,
|
|
21
|
+
} from '@/components/ui/collapsible';
|
|
22
|
+
import Markdown from 'react-markdown';
|
|
23
|
+
import remarkGfm from 'remark-gfm';
|
|
24
|
+
import type { UIMessage } from 'ai';
|
|
25
|
+
import { createCompiler, type Compiler } from '@aprovan/patchwork-compiler';
|
|
26
|
+
import {
|
|
27
|
+
extractCodeBlocks,
|
|
28
|
+
CodePreview,
|
|
29
|
+
MarkdownEditor,
|
|
30
|
+
ServicesInspector,
|
|
31
|
+
type ServiceInfo,
|
|
32
|
+
} from '@aprovan/patchwork-editor';
|
|
33
|
+
|
|
34
|
+
const APROVAN_LOGO =
|
|
35
|
+
'https://raw.githubusercontent.com/AprovanLabs/aprovan.com/main/docs/assets/social-labs.png';
|
|
36
|
+
|
|
37
|
+
interface PatchworkContext {
|
|
38
|
+
compiler: Compiler | null;
|
|
39
|
+
namespaces: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PatchworkCtx = createContext<PatchworkContext>({ compiler: null, namespaces: [] });
|
|
43
|
+
const useCompiler = () => useContext(PatchworkCtx).compiler;
|
|
44
|
+
const useServices = () => useContext(PatchworkCtx).namespaces;
|
|
45
|
+
|
|
46
|
+
function TextPart({ text, isUser }: { text: string; isUser: boolean }) {
|
|
47
|
+
const compiler = useCompiler();
|
|
48
|
+
const services = useServices();
|
|
49
|
+
|
|
50
|
+
if (isUser) {
|
|
51
|
+
return (
|
|
52
|
+
<div className="prose prose-sm prose-invert prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 prose-pre:my-2 prose-code:before:content-none prose-code:after:content-none">
|
|
53
|
+
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const parts = extractCodeBlocks(text);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="prose prose-sm dark:prose-invert prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 prose-pre:my-2 prose-code:before:content-none prose-code:after:content-none">
|
|
62
|
+
{parts.map((part, index) => {
|
|
63
|
+
if (part.type === 'code') {
|
|
64
|
+
return (
|
|
65
|
+
<CodePreview
|
|
66
|
+
key={index}
|
|
67
|
+
code={part.content}
|
|
68
|
+
compiler={compiler}
|
|
69
|
+
services={services}
|
|
70
|
+
filePath={part.attributes?.path}
|
|
71
|
+
entrypoint="main.tsx"
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return <Markdown key={index} remarkPlugins={[remarkGfm]}>{part.content}</Markdown>;
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ReasoningPart({
|
|
82
|
+
text,
|
|
83
|
+
isStreaming,
|
|
84
|
+
}: {
|
|
85
|
+
text: string;
|
|
86
|
+
isStreaming?: boolean;
|
|
87
|
+
}) {
|
|
88
|
+
return (
|
|
89
|
+
<Collapsible defaultOpen={isStreaming}>
|
|
90
|
+
<CollapsibleTrigger className="flex items-center gap-2 text-yellow-700 dark:text-yellow-400 hover:opacity-80 w-full">
|
|
91
|
+
<Brain className="h-4 w-4" />
|
|
92
|
+
<span className="text-xs font-medium">Thinking</span>
|
|
93
|
+
{isStreaming && <Loader2 className="h-3 w-3 animate-spin" />}
|
|
94
|
+
<ChevronDown className="h-3 w-3 ml-auto transition-transform [[data-state=open]>&]:rotate-180" />
|
|
95
|
+
</CollapsibleTrigger>
|
|
96
|
+
<CollapsibleContent>
|
|
97
|
+
<div className="mt-2 p-3 rounded border-l-4 border-yellow-500 bg-yellow-50 dark:bg-yellow-950/50">
|
|
98
|
+
<p className="text-sm text-muted-foreground italic whitespace-pre-wrap">
|
|
99
|
+
{text}
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
</CollapsibleContent>
|
|
103
|
+
</Collapsible>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ToolPart({
|
|
108
|
+
toolName,
|
|
109
|
+
state,
|
|
110
|
+
input,
|
|
111
|
+
output,
|
|
112
|
+
errorText,
|
|
113
|
+
}: {
|
|
114
|
+
toolName: string;
|
|
115
|
+
state: string;
|
|
116
|
+
input: unknown;
|
|
117
|
+
output?: unknown;
|
|
118
|
+
errorText?: string;
|
|
119
|
+
}) {
|
|
120
|
+
const isRunning = state === 'input-streaming' || state === 'input-available';
|
|
121
|
+
const hasError = state === 'output-error';
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Collapsible className="my-1 w-full">
|
|
125
|
+
<CollapsibleTrigger className="inline-flex items-center gap-2 px-3 py-1 rounded-full border bg-muted/50 hover:bg-muted text-xs transition-colors">
|
|
126
|
+
<Wrench className="h-3 w-3 text-muted-foreground" />
|
|
127
|
+
<span className="font-mono">{toolName}</span>
|
|
128
|
+
<span className="w-3 h-3 flex items-center justify-center">
|
|
129
|
+
{isRunning && (
|
|
130
|
+
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
|
131
|
+
)}
|
|
132
|
+
{hasError && <AlertCircle className="h-3 w-3 text-destructive" />}
|
|
133
|
+
</span>
|
|
134
|
+
<ChevronDown className="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-180" />
|
|
135
|
+
</CollapsibleTrigger>
|
|
136
|
+
|
|
137
|
+
<CollapsibleContent className="mt-2 p-3 rounded-lg border bg-white space-y-2">
|
|
138
|
+
{input !== undefined && (
|
|
139
|
+
<div>
|
|
140
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
141
|
+
Input
|
|
142
|
+
</span>
|
|
143
|
+
<div className="mt-1 p-2 bg-muted/30 rounded text-xs overflow-auto max-h-48">
|
|
144
|
+
<pre className="whitespace-pre-wrap break-words m-0">
|
|
145
|
+
{typeof input === 'string'
|
|
146
|
+
? input
|
|
147
|
+
: JSON.stringify(input, null, 2)}
|
|
148
|
+
</pre>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{output !== undefined && (
|
|
154
|
+
<div>
|
|
155
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
156
|
+
Output
|
|
157
|
+
</span>
|
|
158
|
+
<div className="mt-1 p-2 bg-muted/30 rounded text-xs overflow-auto max-h-48">
|
|
159
|
+
<pre className="whitespace-pre-wrap break-words m-0">
|
|
160
|
+
{typeof output === 'string'
|
|
161
|
+
? output
|
|
162
|
+
: JSON.stringify(output, null, 2)}
|
|
163
|
+
</pre>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{errorText && (
|
|
169
|
+
<div className="text-sm text-destructive flex items-center gap-2">
|
|
170
|
+
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
171
|
+
<span className="break-words">{errorText}</span>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</CollapsibleContent>
|
|
175
|
+
</Collapsible>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function MessageBubble({ message }: { message: UIMessage }) {
|
|
180
|
+
const isUser = message.role === 'user';
|
|
181
|
+
const isStreaming = message.parts?.some(
|
|
182
|
+
(p) =>
|
|
183
|
+
'state' in p &&
|
|
184
|
+
(p.state === 'input-streaming' || p.state === 'input-available'),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div className={`flex gap-3 ${isUser ? 'justify-end' : 'justify-start'}`}>
|
|
189
|
+
{!isUser && (
|
|
190
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
191
|
+
<img
|
|
192
|
+
src={APROVAN_LOGO}
|
|
193
|
+
alt="Assistant"
|
|
194
|
+
className="rounded-full"
|
|
195
|
+
/>
|
|
196
|
+
<AvatarFallback className="bg-primary text-primary-foreground">
|
|
197
|
+
A
|
|
198
|
+
</AvatarFallback>
|
|
199
|
+
</Avatar>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
<div
|
|
203
|
+
className={`flex flex-col gap-1 max-w-[80%] min-w-0 ${
|
|
204
|
+
isUser ? 'items-end' : 'items-start'
|
|
205
|
+
}`}
|
|
206
|
+
>
|
|
207
|
+
<div className="flex items-center gap-2 h-5">
|
|
208
|
+
<span className="text-xs text-muted-foreground capitalize">
|
|
209
|
+
{message.role}
|
|
210
|
+
</span>
|
|
211
|
+
{isStreaming && (
|
|
212
|
+
<Badge
|
|
213
|
+
variant="outline"
|
|
214
|
+
className="text-xs"
|
|
215
|
+
>
|
|
216
|
+
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
|
217
|
+
streaming
|
|
218
|
+
</Badge>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div
|
|
223
|
+
className={`rounded-lg px-4 py-2 overflow-hidden w-full ${
|
|
224
|
+
isUser ? 'bg-primary text-primary-foreground' : 'bg-muted'
|
|
225
|
+
}`}
|
|
226
|
+
>
|
|
227
|
+
{message.parts?.map((part, i) => {
|
|
228
|
+
if (part.type === 'text') {
|
|
229
|
+
return (
|
|
230
|
+
<TextPart
|
|
231
|
+
key={i}
|
|
232
|
+
text={part.text}
|
|
233
|
+
isUser={isUser}
|
|
234
|
+
/>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (part.type === 'reasoning') {
|
|
239
|
+
return (
|
|
240
|
+
<ReasoningPart
|
|
241
|
+
key={i}
|
|
242
|
+
text={part.text}
|
|
243
|
+
isStreaming={part.state === 'streaming'}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (part.type.startsWith('tool-') || part.type === 'dynamic-tool') {
|
|
249
|
+
const toolPart = part as {
|
|
250
|
+
type: string;
|
|
251
|
+
toolName?: string;
|
|
252
|
+
toolCallId: string;
|
|
253
|
+
state: string;
|
|
254
|
+
input?: unknown;
|
|
255
|
+
output?: unknown;
|
|
256
|
+
errorText?: string;
|
|
257
|
+
};
|
|
258
|
+
const toolName =
|
|
259
|
+
toolPart.toolName ?? part.type.replace('tool-', '');
|
|
260
|
+
return (
|
|
261
|
+
<ToolPart
|
|
262
|
+
key={i}
|
|
263
|
+
toolName={toolName}
|
|
264
|
+
state={toolPart.state}
|
|
265
|
+
input={toolPart.input}
|
|
266
|
+
output={toolPart.output}
|
|
267
|
+
errorText={toolPart.errorText}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
})}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{isUser && (
|
|
278
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
279
|
+
<AvatarFallback className="bg-secondary">U</AvatarFallback>
|
|
280
|
+
</Avatar>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const PROXY_URL = '/api/proxy';
|
|
287
|
+
const IMAGE_SPEC = '@aprovan/patchwork-image-shadcn';
|
|
288
|
+
// Local proxy for loading image packages, esm.sh for widget imports
|
|
289
|
+
const IMAGE_CDN_URL = import.meta.env.DEV ? '/_local-packages' : 'https://esm.sh';
|
|
290
|
+
const WIDGET_CDN_URL = 'https://esm.sh'; // Widget imports need esm.sh bundles like @packagedcn
|
|
291
|
+
|
|
292
|
+
export default function ChatPage() {
|
|
293
|
+
const [input, setInput] = useState('What\'s the weather in Houston, Texas like?');
|
|
294
|
+
const [compiler, setCompiler] = useState<Compiler | null>(null);
|
|
295
|
+
const [namespaces, setNamespaces] = useState<string[]>([]);
|
|
296
|
+
const [services, setServices] = useState<ServiceInfo[]>([]);
|
|
297
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
298
|
+
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
// Fetch available services
|
|
301
|
+
fetch('/api/services')
|
|
302
|
+
.then((res) => res.json())
|
|
303
|
+
.then((data) => {
|
|
304
|
+
setNamespaces(data.namespaces ?? []);
|
|
305
|
+
// In dev mode, also store full service details for inspection
|
|
306
|
+
if (import.meta.env.DEV && data.services) {
|
|
307
|
+
setServices(data.services);
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
.catch(() => {
|
|
311
|
+
setNamespaces([]);
|
|
312
|
+
setServices([]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Initialize compiler
|
|
316
|
+
createCompiler({
|
|
317
|
+
image: IMAGE_SPEC,
|
|
318
|
+
proxyUrl: PROXY_URL,
|
|
319
|
+
cdnBaseUrl: IMAGE_CDN_URL,
|
|
320
|
+
widgetCdnBaseUrl: WIDGET_CDN_URL,
|
|
321
|
+
})
|
|
322
|
+
.then(setCompiler)
|
|
323
|
+
.catch(console.error);
|
|
324
|
+
}, []);
|
|
325
|
+
|
|
326
|
+
const patchworkCtx = useMemo(() => ({ compiler, namespaces }), [compiler, namespaces]);
|
|
327
|
+
|
|
328
|
+
const transport = useMemo(
|
|
329
|
+
() =>
|
|
330
|
+
new DefaultChatTransport({
|
|
331
|
+
body: () => ({
|
|
332
|
+
metadata: {
|
|
333
|
+
patchwork: { compilers: [IMAGE_SPEC] },
|
|
334
|
+
},
|
|
335
|
+
}),
|
|
336
|
+
}),
|
|
337
|
+
[],
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const { messages, sendMessage, status, error } = useChat({ transport });
|
|
341
|
+
|
|
342
|
+
const isLoading = status === 'submitted' || status === 'streaming';
|
|
343
|
+
|
|
344
|
+
const handleSubmit = useCallback((e?: React.FormEvent) => {
|
|
345
|
+
e?.preventDefault();
|
|
346
|
+
if (!input.trim()) return;
|
|
347
|
+
sendMessage({ text: input });
|
|
348
|
+
setInput('');
|
|
349
|
+
}, [input, sendMessage]);
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
scrollRef.current?.scrollTo({
|
|
353
|
+
top: scrollRef.current.scrollHeight,
|
|
354
|
+
behavior: 'smooth',
|
|
355
|
+
});
|
|
356
|
+
}, [messages]);
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<PatchworkCtx.Provider value={patchworkCtx}>
|
|
360
|
+
<div className="flex flex-col h-screen max-w-4xl mx-auto p-4">
|
|
361
|
+
<Card className="flex-1 flex flex-col min-h-0 overflow-hidden border">
|
|
362
|
+
<CardHeader className="border-b py-3">
|
|
363
|
+
<CardTitle className="flex items-center gap-3">
|
|
364
|
+
<img
|
|
365
|
+
src={APROVAN_LOGO}
|
|
366
|
+
alt="Aprovan"
|
|
367
|
+
className="h-8 w-8 rounded-full"
|
|
368
|
+
/>
|
|
369
|
+
<span className="text-lg">patchwork</span>
|
|
370
|
+
<ServicesInspector namespaces={namespaces} services={services} />
|
|
371
|
+
</CardTitle>
|
|
372
|
+
</CardHeader>
|
|
373
|
+
|
|
374
|
+
<CardContent className="flex-1 p-0 min-h-0">
|
|
375
|
+
<ScrollArea
|
|
376
|
+
className="h-full"
|
|
377
|
+
ref={scrollRef}
|
|
378
|
+
>
|
|
379
|
+
<div className="p-4 space-y-4">
|
|
380
|
+
{messages.length === 0 ? (
|
|
381
|
+
<div className="text-center text-muted-foreground py-12">
|
|
382
|
+
<img
|
|
383
|
+
src={APROVAN_LOGO}
|
|
384
|
+
alt=""
|
|
385
|
+
className="h-12 w-12 mx-auto mb-4 opacity-50 rounded-full"
|
|
386
|
+
/>
|
|
387
|
+
<p>Start a conversation</p>
|
|
388
|
+
</div>
|
|
389
|
+
) : (
|
|
390
|
+
messages.map((msg) => (
|
|
391
|
+
<MessageBubble
|
|
392
|
+
key={msg.id}
|
|
393
|
+
message={msg}
|
|
394
|
+
/>
|
|
395
|
+
))
|
|
396
|
+
)}
|
|
397
|
+
|
|
398
|
+
{isLoading &&
|
|
399
|
+
messages[messages.length - 1]?.role !== 'assistant' && (
|
|
400
|
+
<div className="flex gap-3 justify-start">
|
|
401
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
402
|
+
<img
|
|
403
|
+
src={APROVAN_LOGO}
|
|
404
|
+
alt=""
|
|
405
|
+
className="rounded-full"
|
|
406
|
+
/>
|
|
407
|
+
<AvatarFallback>A</AvatarFallback>
|
|
408
|
+
</Avatar>
|
|
409
|
+
<div className="flex flex-col gap-1">
|
|
410
|
+
<div className="h-5" />
|
|
411
|
+
<div className="bg-muted rounded-lg px-4 py-2">
|
|
412
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</ScrollArea>
|
|
419
|
+
</CardContent>
|
|
420
|
+
|
|
421
|
+
{error && (
|
|
422
|
+
<div className="px-4 py-2 bg-destructive/10 text-destructive text-sm flex items-center gap-2">
|
|
423
|
+
<AlertCircle className="h-4 w-4" />
|
|
424
|
+
{error.message}
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
|
|
428
|
+
<div className="p-4 border-t">
|
|
429
|
+
<form
|
|
430
|
+
onSubmit={handleSubmit}
|
|
431
|
+
className="flex gap-2 items-end"
|
|
432
|
+
>
|
|
433
|
+
<MarkdownEditor
|
|
434
|
+
value={input}
|
|
435
|
+
onChange={setInput}
|
|
436
|
+
onSubmit={() => {
|
|
437
|
+
if (!isLoading && input.trim()) {
|
|
438
|
+
handleSubmit();
|
|
439
|
+
}
|
|
440
|
+
}}
|
|
441
|
+
placeholder="Type a message... (Shift+Enter for new line)"
|
|
442
|
+
disabled={isLoading}
|
|
443
|
+
/>
|
|
444
|
+
<Button
|
|
445
|
+
type="submit"
|
|
446
|
+
disabled={isLoading || !input.trim()}
|
|
447
|
+
className="shrink-0"
|
|
448
|
+
>
|
|
449
|
+
{isLoading ? (
|
|
450
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
451
|
+
) : (
|
|
452
|
+
<Send className="h-4 w-4" />
|
|
453
|
+
)}
|
|
454
|
+
</Button>
|
|
455
|
+
</form>
|
|
456
|
+
</div>
|
|
457
|
+
</Card>
|
|
458
|
+
</div>
|
|
459
|
+
</PatchworkCtx.Provider>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
darkMode: ['class'],
|
|
4
|
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
|
5
|
+
theme: {
|
|
6
|
+
container: {
|
|
7
|
+
center: true,
|
|
8
|
+
padding: '2rem',
|
|
9
|
+
screens: {
|
|
10
|
+
'2xl': '1400px',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
extend: {
|
|
14
|
+
colors: {
|
|
15
|
+
border: 'hsl(var(--border))',
|
|
16
|
+
input: 'hsl(var(--input))',
|
|
17
|
+
ring: 'hsl(var(--ring))',
|
|
18
|
+
background: 'hsl(var(--background))',
|
|
19
|
+
foreground: 'hsl(var(--foreground))',
|
|
20
|
+
primary: {
|
|
21
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
22
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
23
|
+
},
|
|
24
|
+
secondary: {
|
|
25
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
26
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
27
|
+
},
|
|
28
|
+
destructive: {
|
|
29
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
30
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
31
|
+
},
|
|
32
|
+
muted: {
|
|
33
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
34
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
35
|
+
},
|
|
36
|
+
accent: {
|
|
37
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
38
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
39
|
+
},
|
|
40
|
+
popover: {
|
|
41
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
42
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
43
|
+
},
|
|
44
|
+
card: {
|
|
45
|
+
DEFAULT: 'hsl(var(--card))',
|
|
46
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
borderRadius: {
|
|
50
|
+
lg: 'var(--radius)',
|
|
51
|
+
md: 'calc(var(--radius) - 2px)',
|
|
52
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
53
|
+
},
|
|
54
|
+
keyframes: {
|
|
55
|
+
'accordion-down': {
|
|
56
|
+
from: { height: '0' },
|
|
57
|
+
to: { height: 'var(--radix-accordion-content-height)' },
|
|
58
|
+
},
|
|
59
|
+
'accordion-up': {
|
|
60
|
+
from: { height: 'var(--radix-accordion-content-height)' },
|
|
61
|
+
to: { height: '0' },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
animation: {
|
|
65
|
+
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
66
|
+
'accordion-up': 'accordion-up 0.2s ease-out',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
plugins: [require('@tailwindcss/typography')],
|
|
71
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"baseUrl": ".",
|
|
19
|
+
"paths": {
|
|
20
|
+
"@/*": ["./src/*"]
|
|
21
|
+
},
|
|
22
|
+
"types": ["vite/client"]
|
|
23
|
+
},
|
|
24
|
+
"include": ["src"]
|
|
25
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
const STITCHERY_URL = "http://127.0.0.1:6435";
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [react()],
|
|
9
|
+
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
|
|
10
|
+
server: {
|
|
11
|
+
proxy: {
|
|
12
|
+
"/api": {
|
|
13
|
+
target: STITCHERY_URL,
|
|
14
|
+
changeOrigin: true,
|
|
15
|
+
},
|
|
16
|
+
"/_local-packages": {
|
|
17
|
+
target: STITCHERY_URL,
|
|
18
|
+
changeOrigin: true,
|
|
19
|
+
},
|
|
20
|
+
"/vfs": {
|
|
21
|
+
target: STITCHERY_URL,
|
|
22
|
+
changeOrigin: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|