arcie 0.1.5 → 0.1.7

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.
Files changed (36) hide show
  1. package/dist/_tsup-dts-rollup.d.ts +2243 -0
  2. package/dist/agent/index.d.ts +1 -6
  3. package/dist/auth/index.d.ts +4 -11
  4. package/dist/channels/index.d.ts +3 -8
  5. package/dist/cli/index.d.ts +1 -8
  6. package/dist/cli/index.js +14 -3
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/context/index.d.ts +9 -14
  9. package/dist/{dev-ZZDVOWUZ.js → dev-A6KGOSNL.js} +86 -62
  10. package/dist/dev-A6KGOSNL.js.map +1 -0
  11. package/dist/discover/index.d.ts +6 -41
  12. package/dist/hooks/index.d.ts +1 -6
  13. package/dist/index.d.ts +113 -297
  14. package/dist/{init-5F7CIVLN.js → init-DNVKDUHX.js} +3 -3
  15. package/dist/instructions/index.d.ts +2 -7
  16. package/dist/protocol/events.d.ts +44 -200
  17. package/dist/runner/index.d.ts +4 -4
  18. package/dist/schedules/index.d.ts +1 -6
  19. package/dist/skills/index.d.ts +2 -7
  20. package/dist/tools/index.d.ts +3 -13
  21. package/package.json +2 -1
  22. package/templates/web-chat/app/globals.css +10 -0
  23. package/templates/web-chat/app/layout.tsx +1 -1
  24. package/templates/web-chat/app/page.tsx +1 -1
  25. package/templates/web-chat/components/chat.tsx +179 -156
  26. package/templates/web-chat/components/input-bar.tsx +146 -39
  27. package/templates/web-chat/components/message.tsx +90 -30
  28. package/templates/web-chat/components/tool-call.tsx +8 -8
  29. package/templates/web-chat/lib/types.ts +1 -0
  30. package/templates/web-chat/next.config.mjs +3 -1
  31. package/templates/web-chat/package.json +1 -0
  32. package/templates/web-chat/tailwind.config.ts +3 -1
  33. package/dist/dev-ZZDVOWUZ.js.map +0 -1
  34. package/dist/index-CElLRVTP.d.ts +0 -136
  35. package/dist/types-DwxwdSa2.d.ts +0 -151
  36. /package/dist/{init-5F7CIVLN.js.map → init-DNVKDUHX.js.map} +0 -0
@@ -1,12 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import { Zap } from "lucide-react";
5
4
  import { InputBar } from "@/components/input-bar";
6
5
  import { Message } from "@/components/message";
7
6
  import { readArcieStream } from "@/lib/stream";
8
7
  import type { UiMessage, UiToolCall } from "@/lib/types";
9
- import { cn } from "@/lib/utils";
10
8
 
11
9
  function newId(prefix: string): string {
12
10
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -16,10 +14,12 @@ export function Chat() {
16
14
  const [messages, setMessages] = React.useState<UiMessage[]>([]);
17
15
  const [streaming, setStreaming] = React.useState(false);
18
16
  const abortRef = React.useRef<AbortController | undefined>(undefined);
19
- const bottomRef = React.useRef<HTMLDivElement>(null);
17
+ const containerRef = React.useRef<HTMLDivElement>(null);
20
18
 
21
19
  React.useEffect(() => {
22
- bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
20
+ const el = containerRef.current;
21
+ if (el === null) return;
22
+ el.scrollTop = el.scrollHeight;
23
23
  }, [messages]);
24
24
 
25
25
  const stop = () => {
@@ -27,170 +27,193 @@ export function Chat() {
27
27
  setStreaming(false);
28
28
  };
29
29
 
30
- const send = async (text: string) => {
31
- const userMessage: UiMessage = { id: newId("u"), role: "user", content: text };
32
- const assistantId = newId("a");
33
- const assistantMessage: UiMessage = {
34
- id: assistantId,
35
- role: "assistant",
36
- content: "",
37
- streaming: true,
38
- toolCalls: [],
39
- };
40
- setMessages((prev) => [...prev, userMessage, assistantMessage]);
41
- setStreaming(true);
42
-
43
- const controller = new AbortController();
44
- abortRef.current = controller;
45
-
46
- const updateAssistant = (patch: (prev: UiMessage) => UiMessage) => {
47
- setMessages((prev) =>
48
- prev.map((message) => (message.id === assistantId ? patch(message) : message)),
49
- );
50
- };
51
-
52
- try {
53
- const response = await fetch("/api/chat", {
54
- method: "POST",
55
- headers: { "Content-Type": "application/json" },
56
- body: JSON.stringify({ message: text }),
57
- signal: controller.signal,
58
- });
59
- if (!response.ok) {
60
- const errorBody = await response.text();
61
- updateAssistant((m) => ({
62
- ...m,
63
- content: errorBody || `Server error (${response.status})`,
64
- streaming: false,
65
- errored: true,
66
- }));
67
- return;
68
- }
30
+ const clearAll = () => {
31
+ if (streaming) stop();
32
+ setMessages([]);
33
+ };
69
34
 
70
- const toolIndex = new Map<string, number>();
35
+ const copy = (text: string) => {
36
+ void navigator.clipboard.writeText(text);
37
+ };
71
38
 
72
- for await (const event of readArcieStream(response)) {
73
- switch (event.type) {
74
- case "message.appended": {
75
- const delta = (event.data as { delta?: string }).delta ?? "";
76
- updateAssistant((m) => ({ ...m, content: `${m.content}${delta}` }));
77
- break;
78
- }
79
- case "message.completed": {
80
- const text = (event.data as { text?: string | null }).text;
81
- updateAssistant((m) => ({
82
- ...m,
83
- content: typeof text === "string" && text.length > 0 ? text : m.content,
84
- }));
85
- break;
86
- }
87
- case "reasoning.appended": {
88
- const delta = (event.data as { delta?: string }).delta ?? "";
89
- updateAssistant((m) => ({ ...m, reasoning: `${m.reasoning ?? ""}${delta}` }));
90
- break;
91
- }
92
- case "tool.started": {
93
- const data = event.data as { name: string; callId: string; input: unknown };
94
- const call: UiToolCall = {
95
- callId: data.callId,
96
- name: data.name,
97
- input: data.input,
98
- status: "running",
99
- };
100
- updateAssistant((m) => {
101
- const toolCalls = m.toolCalls ?? [];
102
- toolIndex.set(data.callId, toolCalls.length);
103
- return { ...m, toolCalls: [...toolCalls, call] };
104
- });
105
- break;
106
- }
107
- case "tool.completed": {
108
- const data = event.data as {
109
- callId: string;
110
- output: unknown;
111
- status: string;
112
- error?: { code: string; message: string };
113
- };
114
- updateAssistant((m) => {
115
- const toolCalls = [...(m.toolCalls ?? [])];
116
- const idx = toolIndex.get(data.callId);
117
- if (idx === undefined || toolCalls[idx] === undefined) return m;
118
- const previous = toolCalls[idx];
119
- const isApproval =
120
- data.status === "pending" && data.error?.code === "needs_approval";
121
- toolCalls[idx] = {
122
- ...previous,
123
- status: isApproval
124
- ? "approval"
125
- : data.status === "completed"
126
- ? "done"
127
- : "error",
128
- output: data.output,
129
- errorMessage: data.error?.message,
39
+ const send = React.useCallback(
40
+ async (text: string, historyOverride?: UiMessage[]) => {
41
+ const base = historyOverride ?? messages;
42
+ const userMessage: UiMessage = { id: newId("u"), role: "user", content: text };
43
+ const assistantId = newId("a");
44
+ const assistantMessage: UiMessage = {
45
+ id: assistantId,
46
+ role: "assistant",
47
+ content: "",
48
+ streaming: true,
49
+ toolCalls: [],
50
+ };
51
+ const isRegeneration = historyOverride !== undefined;
52
+ setMessages((prev) => (isRegeneration ? [...base, assistantMessage] : [...prev, userMessage, assistantMessage]));
53
+ setStreaming(true);
54
+
55
+ const controller = new AbortController();
56
+ abortRef.current = controller;
57
+ const startedAt = Date.now();
58
+
59
+ const patchAssistant = (patch: (prev: UiMessage) => UiMessage) => {
60
+ setMessages((prev) =>
61
+ prev.map((message) => (message.id === assistantId ? patch(message) : message)),
62
+ );
63
+ };
64
+
65
+ try {
66
+ const response = await fetch("/api/chat", {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({ message: text }),
70
+ signal: controller.signal,
71
+ });
72
+ if (!response.ok) {
73
+ const body = await response.text();
74
+ patchAssistant((m) => ({
75
+ ...m,
76
+ content: body || `Server error (${response.status})`,
77
+ streaming: false,
78
+ errored: true,
79
+ }));
80
+ return;
81
+ }
82
+
83
+ const toolIndex = new Map<string, number>();
84
+
85
+ for await (const event of readArcieStream(response)) {
86
+ switch (event.type) {
87
+ case "message.appended": {
88
+ const delta = (event.data as { delta?: string }).delta ?? "";
89
+ patchAssistant((m) => ({ ...m, content: `${m.content}${delta}` }));
90
+ break;
91
+ }
92
+ case "message.completed": {
93
+ const finished = (event.data as { text?: string | null }).text;
94
+ patchAssistant((m) => ({
95
+ ...m,
96
+ content: typeof finished === "string" && finished.length > 0 ? finished : m.content,
97
+ }));
98
+ break;
99
+ }
100
+ case "reasoning.appended": {
101
+ const delta = (event.data as { delta?: string }).delta ?? "";
102
+ patchAssistant((m) => ({ ...m, reasoning: `${m.reasoning ?? ""}${delta}` }));
103
+ break;
104
+ }
105
+ case "tool.started": {
106
+ const data = event.data as { name: string; callId: string; input: unknown };
107
+ const call: UiToolCall = {
108
+ callId: data.callId,
109
+ name: data.name,
110
+ input: data.input,
111
+ status: "running",
130
112
  };
131
- return { ...m, toolCalls };
132
- });
133
- break;
134
- }
135
- case "step.failed":
136
- case "turn.failed":
137
- case "session.failed": {
138
- const data = event.data as { code?: string; message?: string };
139
- updateAssistant((m) => ({
140
- ...m,
141
- content: data.message ?? "Something went wrong.",
142
- streaming: false,
143
- errored: true,
144
- }));
145
- break;
113
+ patchAssistant((m) => {
114
+ const toolCalls = m.toolCalls ?? [];
115
+ toolIndex.set(data.callId, toolCalls.length);
116
+ return { ...m, toolCalls: [...toolCalls, call] };
117
+ });
118
+ break;
119
+ }
120
+ case "tool.completed": {
121
+ const data = event.data as {
122
+ callId: string;
123
+ output: unknown;
124
+ status: string;
125
+ error?: { code: string; message: string };
126
+ };
127
+ patchAssistant((m) => {
128
+ const toolCalls = [...(m.toolCalls ?? [])];
129
+ const idx = toolIndex.get(data.callId);
130
+ if (idx === undefined || toolCalls[idx] === undefined) return m;
131
+ const previous = toolCalls[idx];
132
+ const isApproval =
133
+ data.status === "pending" && data.error?.code === "needs_approval";
134
+ toolCalls[idx] = {
135
+ ...previous,
136
+ status: isApproval
137
+ ? "approval"
138
+ : data.status === "completed"
139
+ ? "done"
140
+ : "error",
141
+ output: data.output,
142
+ errorMessage: data.error?.message,
143
+ };
144
+ return { ...m, toolCalls };
145
+ });
146
+ break;
147
+ }
148
+ case "step.failed":
149
+ case "turn.failed":
150
+ case "session.failed": {
151
+ const data = event.data as { code?: string; message?: string };
152
+ patchAssistant((m) => ({
153
+ ...m,
154
+ content: data.message ?? "Something went wrong.",
155
+ streaming: false,
156
+ errored: true,
157
+ }));
158
+ break;
159
+ }
146
160
  }
147
161
  }
162
+ } catch (error) {
163
+ if (!controller.signal.aborted) {
164
+ patchAssistant((m) => ({
165
+ ...m,
166
+ content: error instanceof Error ? error.message : String(error),
167
+ streaming: false,
168
+ errored: true,
169
+ }));
170
+ }
171
+ } finally {
172
+ const latencyMs = Date.now() - startedAt;
173
+ patchAssistant((m) => ({ ...m, streaming: false, latencyMs }));
174
+ setStreaming(false);
175
+ abortRef.current = undefined;
148
176
  }
149
- } catch (error) {
150
- if (controller.signal.aborted) {
151
- updateAssistant((m) => ({ ...m, streaming: false }));
152
- } else {
153
- updateAssistant((m) => ({
154
- ...m,
155
- content: error instanceof Error ? error.message : String(error),
156
- streaming: false,
157
- errored: true,
158
- }));
159
- }
160
- } finally {
161
- updateAssistant((m) => ({ ...m, streaming: false }));
162
- setStreaming(false);
163
- abortRef.current = undefined;
164
- }
177
+ },
178
+ [messages],
179
+ );
180
+
181
+ const regenerate = () => {
182
+ const lastUserIndex = messages.map((m) => m.role).lastIndexOf("user");
183
+ if (lastUserIndex === -1) return;
184
+ const historyBefore = messages.slice(0, lastUserIndex);
185
+ const lastUser = messages[lastUserIndex]!;
186
+ setMessages([...historyBefore, lastUser]);
187
+ void send(lastUser.content, [...historyBefore, lastUser]);
165
188
  };
166
189
 
167
190
  return (
168
- <div className="flex h-full flex-col">
169
- <header className="flex items-center gap-2 border-b border-border px-4 py-3">
170
- <Zap className="h-5 w-5 text-amber-500" />
171
- <span className="font-semibold">arcie</span>
172
- <span className="text-xs text-muted-foreground">web chat</span>
173
- </header>
174
- <div className="flex-1 overflow-y-auto">
175
- <div className="mx-auto flex max-w-3xl flex-col gap-3 px-4 py-6">
176
- {messages.length === 0 && <EmptyState />}
177
- {messages.map((message) => (
178
- <Message key={message.id} message={message} />
179
- ))}
180
- <div ref={bottomRef} />
191
+ <div className="fixed inset-0 flex flex-col overflow-hidden bg-background text-foreground">
192
+ <div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
193
+ <div className="mx-auto flex min-h-full w-full max-w-3xl flex-col justify-start gap-6 pt-8 pb-4">
194
+ {messages.map((message, index) => {
195
+ const isLast =
196
+ message.role === "assistant" && index === messages.length - 1 && !streaming;
197
+ return (
198
+ <Message
199
+ key={message.id}
200
+ message={message}
201
+ isLast={isLast}
202
+ onCopy={copy}
203
+ onRegenerate={regenerate}
204
+ />
205
+ );
206
+ })}
181
207
  </div>
182
208
  </div>
183
- <InputBar onSend={send} onStop={stop} streaming={streaming} />
184
- </div>
185
- );
186
- }
187
209
 
188
- function EmptyState() {
189
- return (
190
- <div className={cn("mt-24 flex flex-col items-center gap-2 text-center text-muted-foreground")}>
191
- <Zap className="h-8 w-8 text-amber-500" />
192
- <div className="text-lg font-medium text-foreground">Start a conversation</div>
193
- <div className="text-sm">Ask your arcie agent anything.</div>
210
+ <InputBar
211
+ onSend={(text) => void send(text)}
212
+ onStop={stop}
213
+ onClear={clearAll}
214
+ streaming={streaming}
215
+ hasMessages={messages.length > 0}
216
+ />
194
217
  </div>
195
218
  );
196
219
  }
@@ -1,26 +1,39 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import { Send, StopCircle } from "lucide-react";
5
- import { Button } from "@/components/ui/button";
4
+ import { ArrowUp, Paperclip, StopCircle, Trash2, X } from "lucide-react";
6
5
  import { cn } from "@/lib/utils";
7
6
 
8
7
  interface InputBarProps {
9
- onSend(message: string): void;
8
+ onSend(message: string, files?: File[]): void;
10
9
  onStop(): void;
10
+ onClear?(): void;
11
11
  streaming: boolean;
12
12
  disabled?: boolean;
13
+ hasMessages?: boolean;
13
14
  }
14
15
 
15
- export function InputBar({ onSend, onStop, streaming, disabled }: InputBarProps) {
16
+ export function InputBar({
17
+ onSend,
18
+ onStop,
19
+ onClear,
20
+ streaming,
21
+ disabled,
22
+ hasMessages,
23
+ }: InputBarProps) {
16
24
  const [value, setValue] = React.useState("");
25
+ const [files, setFiles] = React.useState<File[]>([]);
17
26
  const textareaRef = React.useRef<HTMLTextAreaElement>(null);
27
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
18
28
 
19
29
  const submit = () => {
20
30
  const trimmed = value.trim();
21
- if (trimmed.length === 0 || streaming || disabled) return;
22
- onSend(trimmed);
31
+ if ((trimmed.length === 0 && files.length === 0) || streaming || disabled) return;
32
+ onSend(trimmed, files.length > 0 ? files : undefined);
23
33
  setValue("");
34
+ setFiles([]);
35
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
36
+ if (fileInputRef.current) fileInputRef.current.value = "";
24
37
  };
25
38
 
26
39
  const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -30,44 +43,138 @@ export function InputBar({ onSend, onStop, streaming, disabled }: InputBarProps)
30
43
  }
31
44
  };
32
45
 
33
- React.useEffect(() => {
34
- const el = textareaRef.current;
35
- if (el === null) return;
46
+ const onChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
47
+ setValue(event.target.value);
48
+ const el = event.target;
36
49
  el.style.height = "auto";
37
- el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
38
- }, [value]);
50
+ el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
51
+ };
52
+
53
+ const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
54
+ const next = event.target.files;
55
+ if (next === null) return;
56
+ setFiles((prev) => [...prev, ...Array.from(next)]);
57
+ };
58
+
59
+ const removeFile = (index: number) => {
60
+ setFiles((prev) => prev.filter((_, i) => i !== index));
61
+ };
62
+
63
+ const openFilePicker = () => {
64
+ fileInputRef.current?.click();
65
+ };
39
66
 
40
67
  return (
41
- <div className="border-t border-border bg-background px-4 py-3">
42
- <div className="mx-auto flex max-w-3xl items-end gap-2">
43
- <textarea
44
- ref={textareaRef}
45
- value={value}
46
- onChange={(event) => setValue(event.target.value)}
47
- onKeyDown={onKeyDown}
48
- rows={1}
49
- placeholder={streaming ? "…" : "Send a message"}
50
- disabled={disabled}
68
+ <div className="shrink-0 bg-transparent px-4 pt-4 pb-6">
69
+ <div className="mx-auto w-full max-w-3xl">
70
+ <div
51
71
  className={cn(
52
- "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground",
53
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
54
- "disabled:cursor-not-allowed disabled:opacity-50",
72
+ "relative flex flex-col rounded-2xl border border-border/40 bg-muted/10 backdrop-blur-md p-3 transition-all",
73
+ "hover:border-border/60 hover:bg-muted/20",
74
+ "focus-within:border-primary/45 focus-within:ring-1 focus-within:ring-primary/20",
75
+ )}
76
+ >
77
+ {files.length > 0 && (
78
+ <div className="flex flex-wrap gap-1.5 mb-2">
79
+ {files.map((file, index) => (
80
+ <div
81
+ key={`${file.name}-${index}`}
82
+ className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-card/60 text-[11px] font-medium text-foreground border border-border/30"
83
+ >
84
+ <Paperclip className="h-3 w-3 text-muted-foreground/70" />
85
+ <span className="max-w-[160px] truncate">{file.name}</span>
86
+ <button
87
+ type="button"
88
+ onClick={() => removeFile(index)}
89
+ className="text-muted-foreground/50 hover:text-foreground transition-colors"
90
+ title="Remove attachment"
91
+ >
92
+ <X className="h-3 w-3" />
93
+ </button>
94
+ </div>
95
+ ))}
96
+ </div>
55
97
  )}
56
- />
57
- {streaming ? (
58
- <Button size="icon" variant="outline" onClick={onStop} aria-label="Stop">
59
- <StopCircle className="h-4 w-4" />
60
- </Button>
61
- ) : (
62
- <Button
63
- size="icon"
64
- onClick={submit}
65
- disabled={value.trim().length === 0 || disabled}
66
- aria-label="Send"
67
- >
68
- <Send className="h-4 w-4" />
69
- </Button>
70
- )}
98
+
99
+ <textarea
100
+ ref={textareaRef}
101
+ value={value}
102
+ onChange={onChange}
103
+ onKeyDown={onKeyDown}
104
+ rows={2}
105
+ placeholder={streaming ? "" : "Ask a question..."}
106
+ disabled={disabled}
107
+ className={cn(
108
+ "max-h-40 min-h-[48px] w-full resize-none bg-transparent py-1.5 text-xs",
109
+ "placeholder:text-muted-foreground/50 focus:outline-none leading-relaxed",
110
+ "disabled:cursor-not-allowed disabled:opacity-50",
111
+ )}
112
+ />
113
+
114
+ <div className="flex items-center justify-between pt-2.5 mt-2 select-none">
115
+ <div className="flex items-center gap-1.5">
116
+ <input
117
+ ref={fileInputRef}
118
+ type="file"
119
+ multiple
120
+ onChange={onFileChange}
121
+ className="hidden"
122
+ />
123
+ <button
124
+ type="button"
125
+ onClick={openFilePicker}
126
+ disabled={streaming}
127
+ className={cn(
128
+ "h-8 w-8 flex items-center justify-center rounded-full text-muted-foreground/60 transition-colors",
129
+ "hover:bg-muted/30 hover:text-foreground",
130
+ "disabled:cursor-not-allowed disabled:opacity-40",
131
+ )}
132
+ aria-label="Attach files"
133
+ title="Attach files"
134
+ >
135
+ <Paperclip className="h-4 w-4" />
136
+ </button>
137
+ </div>
138
+
139
+ <div className="flex items-center gap-2 shrink-0">
140
+ {hasMessages && onClear && (
141
+ <button
142
+ type="button"
143
+ onClick={onClear}
144
+ className="h-8 w-8 flex items-center justify-center rounded-full text-muted-foreground/60 hover:bg-muted/30 hover:text-foreground transition-colors"
145
+ aria-label="New chat"
146
+ title="New chat"
147
+ >
148
+ <Trash2 className="h-4 w-4" />
149
+ </button>
150
+ )}
151
+ {streaming ? (
152
+ <button
153
+ type="button"
154
+ onClick={onStop}
155
+ className="h-8 w-8 flex items-center justify-center rounded-full bg-foreground text-background hover:bg-foreground/90 transition-colors"
156
+ aria-label="Stop generation"
157
+ >
158
+ <StopCircle className="h-3.5 w-3.5 fill-current" />
159
+ </button>
160
+ ) : (
161
+ <button
162
+ type="button"
163
+ onClick={submit}
164
+ disabled={(value.trim().length === 0 && files.length === 0) || disabled}
165
+ className={cn(
166
+ "h-8 w-8 flex items-center justify-center rounded-full bg-foreground text-background transition-colors",
167
+ "hover:bg-foreground/90",
168
+ "disabled:cursor-not-allowed disabled:opacity-40",
169
+ )}
170
+ aria-label="Send message"
171
+ >
172
+ <ArrowUp className="h-3.5 w-3.5" />
173
+ </button>
174
+ )}
175
+ </div>
176
+ </div>
177
+ </div>
71
178
  </div>
72
179
  </div>
73
180
  );