@alquimia-ai/ui 1.2.0 → 1.2.1

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 (104) hide show
  1. package/dist/components/hooks/index.js +1 -1
  2. package/dist/components/hooks/index.js.map +1 -1
  3. package/dist/components/hooks/index.mjs +1 -1
  4. package/dist/components/hooks/index.mjs.map +1 -1
  5. package/dist/components/molecules/index.d.mts +15 -2
  6. package/dist/components/molecules/index.d.ts +15 -2
  7. package/dist/components/molecules/index.js +837 -725
  8. package/dist/components/molecules/index.js.map +1 -1
  9. package/dist/components/molecules/index.mjs +819 -707
  10. package/dist/components/molecules/index.mjs.map +1 -1
  11. package/dist/components/organisms/index.js +260 -149
  12. package/dist/components/organisms/index.js.map +1 -1
  13. package/dist/components/organisms/index.mjs +258 -147
  14. package/dist/components/organisms/index.mjs.map +1 -1
  15. package/dist/index.d.mts +2 -2
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +265 -153
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +263 -151
  20. package/dist/index.mjs.map +1 -1
  21. package/package.json +52 -32
  22. package/src/components/atoms/index.ts +32 -0
  23. package/src/components/atoms/ui/alert.tsx +59 -0
  24. package/src/components/atoms/ui/aspect-ratio.tsx +7 -0
  25. package/src/components/atoms/ui/avatar.tsx +50 -0
  26. package/src/components/atoms/ui/badge.tsx +36 -0
  27. package/src/components/atoms/ui/breadcrumb.tsx +120 -0
  28. package/src/components/atoms/ui/button.tsx +56 -0
  29. package/src/components/atoms/ui/card.tsx +87 -0
  30. package/src/components/atoms/ui/checkbox.tsx +31 -0
  31. package/src/components/atoms/ui/command.tsx +155 -0
  32. package/src/components/atoms/ui/dialog.tsx +125 -0
  33. package/src/components/atoms/ui/drawer.tsx +119 -0
  34. package/src/components/atoms/ui/input.tsx +26 -0
  35. package/src/components/atoms/ui/label.tsx +26 -0
  36. package/src/components/atoms/ui/loader.tsx +52 -0
  37. package/src/components/atoms/ui/popover.tsx +31 -0
  38. package/src/components/atoms/ui/rich-text.tsx +19 -0
  39. package/src/components/atoms/ui/scroll-area.tsx +48 -0
  40. package/src/components/atoms/ui/select.tsx +160 -0
  41. package/src/components/atoms/ui/skeleton.tsx +15 -0
  42. package/src/components/atoms/ui/slider.tsx +29 -0
  43. package/src/components/atoms/ui/switch.tsx +30 -0
  44. package/src/components/atoms/ui/table.tsx +118 -0
  45. package/src/components/atoms/ui/tabs.tsx +56 -0
  46. package/src/components/atoms/ui/text-area/index.tsx +24 -0
  47. package/src/components/atoms/ui/textarea.tsx +25 -0
  48. package/src/components/atoms/ui/think-indicator.tsx +103 -0
  49. package/src/components/atoms/ui/toast.tsx +129 -0
  50. package/src/components/atoms/ui/toaster.tsx +38 -0
  51. package/src/components/atoms/ui/toggle.tsx +45 -0
  52. package/src/components/atoms/ui/typography/index.tsx +30 -0
  53. package/src/components/hooks/index.ts +4 -0
  54. package/src/components/hooks/use-document.tsx +44 -0
  55. package/src/components/hooks/use-resize-observer.ts +28 -0
  56. package/src/components/hooks/use-text-streaming.ts +63 -0
  57. package/src/components/hooks/use-toast.ts +194 -0
  58. package/src/components/index.ts +1 -0
  59. package/src/components/molecules/alert-dialog.tsx +141 -0
  60. package/src/components/molecules/assistant-button.tsx +148 -0
  61. package/src/components/molecules/call-out.tsx +163 -0
  62. package/src/components/molecules/carousel.tsx +262 -0
  63. package/src/components/molecules/documents/document-selector.tsx +79 -0
  64. package/src/components/molecules/documents/document-viewer.tsx +85 -0
  65. package/src/components/molecules/documents/index.ts +2 -0
  66. package/src/components/molecules/index.ts +11 -0
  67. package/src/components/molecules/navigation-menu.tsx +128 -0
  68. package/src/components/molecules/page-container.tsx +17 -0
  69. package/src/components/molecules/rating-comment.tsx +93 -0
  70. package/src/components/molecules/rating-stars.tsx +136 -0
  71. package/src/components/molecules/rating-thumbs.tsx +90 -0
  72. package/src/components/molecules/sidebar.tsx +107 -0
  73. package/src/components/molecules/sonner.tsx +30 -0
  74. package/src/components/molecules/viewers/index.ts +2 -0
  75. package/src/components/molecules/viewers/pdf-viewer.tsx +138 -0
  76. package/src/components/molecules/viewers/plain-text-viewer.tsx +40 -0
  77. package/src/components/organisms/assistant.tsx +271 -0
  78. package/src/components/organisms/index.ts +6 -0
  79. package/src/components/organisms/rating-dialog.tsx +104 -0
  80. package/src/components/organisms/speechToText.tsx +92 -0
  81. package/src/components/organisms/whisper.tsx +106 -0
  82. package/src/components/templates/cards/index.ts +2 -0
  83. package/src/components/templates/cards/with-image-heading-description-avatar.tsx +94 -0
  84. package/src/components/templates/cards/with-image-heading-description.tsx +63 -0
  85. package/src/components/templates/hero/index.tsx +39 -0
  86. package/src/components/templates/index.ts +4 -0
  87. package/src/components/templates/messages-window.tsx +15 -0
  88. package/src/components/templates/query-box.tsx +13 -0
  89. package/src/components/ui/input.tsx +25 -0
  90. package/src/components/ui/select.tsx +163 -0
  91. package/src/index.ts +7 -0
  92. package/src/lib/index.ts +1 -0
  93. package/src/lib/utils.ts +34 -0
  94. package/src/styles/call-out.css +153 -0
  95. package/src/styles/drawer.css +28 -0
  96. package/src/styles/globals.css +69 -0
  97. package/src/styles/prose.css +51 -0
  98. package/src/styles/ratings.css +27 -0
  99. package/src/styles/themes/base-alquimia.css +95 -0
  100. package/src/styles/themes/base-nordic.css +83 -0
  101. package/src/styles/themes/base-primary.css +85 -0
  102. package/src/styles/themes/base.css +8 -0
  103. package/src/types/index.ts +1 -0
  104. package/src/types/type.ts +76 -0
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useTheme } from "next-themes";
4
+ import { Toaster as Sonner } from "sonner";
5
+
6
+ type ToasterProps = React.ComponentProps<typeof Sonner>;
7
+
8
+ const Toaster = ({ ...props }: ToasterProps) => {
9
+ const { theme = "system" } = useTheme();
10
+
11
+ return (
12
+ <Sonner
13
+ theme={theme as ToasterProps["theme"]}
14
+ className="toaster group"
15
+ toastOptions={{
16
+ classNames: {
17
+ toast:
18
+ "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
19
+ description: "group-[.toast]:text-muted-foreground",
20
+ actionButton:
21
+ "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
22
+ cancelButton:
23
+ "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
24
+ },
25
+ }}
26
+ {...props}
27
+ />
28
+ );
29
+ };
30
+ export { Toaster as SonnerToaster };
@@ -0,0 +1,2 @@
1
+ export * from "./pdf-viewer";
2
+ export * from "./plain-text-viewer";
@@ -0,0 +1,138 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useCallback, useEffect } from 'react'
4
+ import { Button, Loader } from '../../atoms';
5
+ import { ChevronLeft, ChevronRight, ZoomIn, ZoomOut, Download } from 'lucide-react'
6
+ import useResizeObserver from '../../hooks/use-resize-observer';
7
+ import { pdfjs, Document, Page } from 'react-pdf';
8
+ import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
9
+ import 'react-pdf/dist/esm/Page/TextLayer.css';
10
+
11
+ pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
12
+
13
+ const options = {
14
+ cMapUrl: '/cmaps/',
15
+ standardFontDataUrl: '/standard_fonts/',
16
+ cMapPacked: true,
17
+ };
18
+
19
+ const resizeObserverOptions = {};
20
+
21
+ type PdfViewerProps = {
22
+ doc: Blob | null;
23
+ loading?: boolean;
24
+ docId: string;
25
+ }
26
+
27
+ export default function PdfViewer({ doc, docId }: PdfViewerProps) {
28
+ const [numPages, setNumPages] = useState<number>(0);
29
+ const [pageNumber, setPageNumber] = useState<number>(1);
30
+ const [scale, setScale] = useState<number>(1);
31
+ const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
32
+ const [containerWidth, setContainerWidth] = useState<number>();
33
+
34
+ const onResize = useCallback<ResizeObserverCallback>((entries) => {
35
+ const [entry] = entries;
36
+ if (entry) {
37
+ setContainerWidth(entry.contentRect.width);
38
+ }
39
+ }, []);
40
+
41
+ useResizeObserver(containerRef, resizeObserverOptions, onResize);
42
+
43
+ function onDocumentLoadSuccess({ numPages: nextNumPages }: pdfjs.PDFDocumentProxy): void {
44
+ setNumPages(nextNumPages);
45
+ }
46
+
47
+ const resetDocument = () => {
48
+ setPageNumber(1);
49
+ setScale(1);
50
+ }
51
+
52
+ useEffect(() => {
53
+ resetDocument()
54
+ }, []);
55
+
56
+
57
+ const handlePrevPage = () => setPageNumber((prev) => Math.max(prev - 1, 1));
58
+ const handleNextPage = () => setPageNumber((prev) => Math.min(prev + 1, numPages));
59
+ const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2));
60
+ const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5));
61
+
62
+ const handleKeyDown = useCallback((e: KeyboardEvent) => {
63
+ if (e.key === 'ArrowLeft') {
64
+ handlePrevPage();
65
+ } else if (e.key === 'ArrowRight') {
66
+ handleNextPage();
67
+ }
68
+ }, [handlePrevPage, handleNextPage]);
69
+
70
+ useEffect(() => {
71
+ window.addEventListener('keydown', handleKeyDown);
72
+ return () => window.removeEventListener('keydown', handleKeyDown);
73
+ }, [handleKeyDown]);
74
+
75
+
76
+ const handleDownloadFile = () => {
77
+ const docUrl = doc && URL.createObjectURL(doc);
78
+ const a = document.createElement('a');
79
+ a.href = docUrl || '';
80
+ a.download = docId;
81
+ a.click();
82
+ URL.revokeObjectURL(docUrl || '');
83
+ };
84
+
85
+ return (
86
+ <>
87
+ <div className="flex justify-between items-center p-4 border-b border-border">
88
+ <div>
89
+ <Button
90
+ onClick={handlePrevPage}
91
+ disabled={pageNumber <= 1}
92
+ >
93
+ <ChevronLeft className="h-4 w-4" />
94
+ </Button>
95
+ <span className="mx-2">
96
+ Page {pageNumber} of {numPages}
97
+ </span>
98
+ <Button
99
+ onClick={handleNextPage}
100
+ disabled={pageNumber >= numPages}
101
+ >
102
+ <ChevronRight className="h-4 w-4" />
103
+ </Button>
104
+ </div>
105
+ {/* <div>
106
+ <Button onClick={handleDownloadFile}>
107
+ <Download className="h-4 w-4" />
108
+ </Button>
109
+ </div> */}
110
+ <div>
111
+ <Button onClick={handleZoomOut}>
112
+ <ZoomOut className="h-4 w-4" />
113
+ </Button>
114
+ <span className="mx-2">{(scale * 100).toFixed(0)}%</span>
115
+ <Button onClick={handleZoomIn}>
116
+ <ZoomIn className="h-4 w-4" />
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ <div ref={setContainerRef} className="overflow-auto">
121
+ <Document
122
+ file={doc}
123
+ // file={`/docs/${docId}`}
124
+ onLoadSuccess={onDocumentLoadSuccess}
125
+ options={options}
126
+ loading={<Loader size="xl" colorVariant="destructive" />}
127
+ >
128
+ <Page
129
+ pageNumber={pageNumber}
130
+ scale={scale}
131
+ width={containerWidth}
132
+ className="w-full"
133
+ />
134
+ </Document>
135
+ </div>
136
+ </>
137
+ );
138
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import { Loader, RichText } from "../../atoms";
5
+
6
+ type PlainTextViewerProps = {
7
+ doc: Blob | null;
8
+ docId?: string;
9
+ };
10
+
11
+ export default function PlainTextViewer({ doc }: PlainTextViewerProps) {
12
+ const [content, setContent] = useState<string | null>(null);
13
+ const [loading, setLoading] = useState(true);
14
+
15
+ useEffect(() => {
16
+ async function parseDocument() {
17
+ try {
18
+ if (doc && doc.type === "text/plain") {
19
+ const text = await doc.text();
20
+ setContent(text);
21
+ }
22
+ } catch (err) {
23
+ console.error("Error parsing Document:", err);
24
+ } finally {
25
+ setLoading(false);
26
+ }
27
+ }
28
+
29
+ parseDocument();
30
+ }, [doc]);
31
+
32
+ return (
33
+ <>
34
+ <div className="relative w-full h-full max-h-[80vh] overflow-y-auto p-8 alq--prose">
35
+ {loading && <Loader />}
36
+ {!loading && <RichText content={content || ""} />}
37
+ </div>
38
+ </>
39
+ );
40
+ }
@@ -0,0 +1,271 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { useEffect } from "react";
5
+ import { AlquimiaSDK } from "@alquimia-ai/tools/sdk";
6
+ import { Typography } from "../atoms";
7
+ import { CallOut, CallOutResponse, CallOutActions } from "../molecules";
8
+ import { ThinkIndicator } from "../atoms";
9
+ import { cn } from "../../lib/utils";
10
+ import { Message } from "ai";
11
+ import { Send } from "lucide-react";
12
+
13
+ interface AssistantProps extends React.HTMLAttributes<HTMLDivElement> {
14
+ sdk: AlquimiaSDK;
15
+ title?: string;
16
+ description?: string;
17
+ }
18
+
19
+ const Assistant = React.forwardRef<HTMLDivElement, AssistantProps>(
20
+ ({ className, sdk, title, description, children, ...props }, ref) => {
21
+ return (
22
+ <div
23
+ ref={ref}
24
+ className={cn("flex flex-col h-full bg-background text-foreground", className)}
25
+ {...props}
26
+ >
27
+ {(title || description) && (
28
+ <AssistantHeader>
29
+ {title && <AssistantTitle title={title} />}
30
+ {description && <AssistantDescription description={description} />}
31
+ </AssistantHeader>
32
+ )}
33
+ {children}
34
+ </div>
35
+ );
36
+ }
37
+ );
38
+ Assistant.displayName = "Assistant";
39
+
40
+ interface AssistantTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
41
+ title: string;
42
+ }
43
+
44
+ const AssistantTitle = React.forwardRef<HTMLHeadingElement, AssistantTitleProps>(
45
+ ({ className, title, ...props }, ref) => (
46
+ <div ref={ref} className={cn("text-foreground", className)} {...props}>
47
+ <Typography typeStyle="heading1" as="h1">
48
+ {title}
49
+ </Typography>
50
+ </div>
51
+ )
52
+ );
53
+ AssistantTitle.displayName = "AssistantTitle";
54
+
55
+ interface AssistantDescriptionProps
56
+ extends React.HTMLAttributes<HTMLParagraphElement> {
57
+ description: string;
58
+ }
59
+
60
+ const AssistantDescription = React.forwardRef<
61
+ HTMLParagraphElement,
62
+ AssistantDescriptionProps
63
+ >(({ className, description, ...props }, ref) => (
64
+ <div ref={ref} className={cn("text-foreground", className)} {...props}>
65
+ <Typography typeStyle="display" as="p">
66
+ {description}
67
+ </Typography>
68
+ </div>
69
+ ));
70
+ AssistantDescription.displayName = "AssistantDescription";
71
+
72
+ interface AssistantHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
73
+
74
+ const AssistantHeader = React.forwardRef<HTMLDivElement, AssistantHeaderProps>(
75
+ ({ className, children, ...props }, ref) => (
76
+ <div
77
+ ref={ref}
78
+ className={cn("bg-background text-foreground", className)}
79
+ {...props}
80
+ >
81
+ {children}
82
+ </div>
83
+ )
84
+ );
85
+ AssistantHeader.displayName = "AssistantHeader";
86
+
87
+ interface MessageAction {
88
+ label: string;
89
+ icon: React.ReactNode;
90
+ onClick: (message?: Message) => Promise<void>;
91
+ }
92
+
93
+ interface AssistantMessageAreaProps
94
+ extends React.HTMLAttributes<HTMLDivElement> {
95
+ messages: (Message & {
96
+ error_code?: string;
97
+ created_at?: string;
98
+ error_detail?: string;
99
+ additionalInfo?: string;
100
+ loading?: boolean;
101
+ })[];
102
+ actions?: MessageAction[];
103
+ messagesEndRef: any;
104
+ thinkIndicator?: React.ReactNode;
105
+ isMessageStreaming?: boolean;
106
+ streamingMessageId: string | null;
107
+ handleIsTextStreaming?: (isStreaming: boolean) => void;
108
+ }
109
+
110
+ const AssistantMessageArea = React.forwardRef<
111
+ HTMLDivElement,
112
+ AssistantMessageAreaProps
113
+ >(
114
+ (
115
+ {
116
+ className,
117
+ messages,
118
+ messagesEndRef,
119
+ actions,
120
+ streamingMessageId,
121
+ thinkIndicator,
122
+ handleIsTextStreaming,
123
+ ...props
124
+ },
125
+ ref
126
+ ) => {
127
+ useEffect(() => {
128
+ messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
129
+ }, []);
130
+ return(
131
+ <div
132
+ ref={ref}
133
+ className={cn("p-6 overflow-y-auto flex-grow bg-background", className)}
134
+ {...props}
135
+ >
136
+ {messages.map((message) => (
137
+ <div key={message.id}>
138
+ <CallOut
139
+ key={message.id}
140
+ role={message.role as "user" | "assistant"}
141
+ message={message}
142
+ >
143
+ {message.created_at && (
144
+ <div className="text-xs text-muted-foreground/80 pb-1">
145
+ {new Date(Number(message.created_at)).toLocaleString("es-ES", {
146
+ day: "2-digit",
147
+ month: "2-digit",
148
+ year: "numeric",
149
+ hour: "2-digit",
150
+ minute: "2-digit",
151
+ second: "2-digit",
152
+ hour12: false,
153
+ })}
154
+ </div>
155
+ )}
156
+ {message.loading && (
157
+ <div>
158
+ {thinkIndicator ?? <ThinkIndicator />}
159
+ </div>
160
+ )}
161
+ {message.error_code && (
162
+ <CallOutResponse
163
+ role="assistant"
164
+ data-error-code={message.error_code}
165
+ >
166
+ We have an internal error, please try again later.
167
+ </CallOutResponse>
168
+ )}
169
+ {message.content && (
170
+ <CallOutResponse
171
+ role={message.role}
172
+ isStreaming={streamingMessageId === message.id}
173
+ handleIsTextStreaming={handleIsTextStreaming}
174
+ additionalInfo={
175
+ streamingMessageId !== message.id
176
+ ? message?.additionalInfo
177
+ : undefined
178
+ }
179
+ >
180
+ {message.content}
181
+ </CallOutResponse>
182
+ )}
183
+ {actions && streamingMessageId !== message.id &&
184
+ <CallOutActions
185
+ key={message.id}
186
+ actions={actions}
187
+ message={message}
188
+ role={message.role as "user" | "assistant"}
189
+ />
190
+ }
191
+ </CallOut>
192
+ </div>
193
+ ))}
194
+ <div ref={messagesEndRef} />
195
+ </div>
196
+ );
197
+ }
198
+ );
199
+ AssistantMessageArea.displayName = "AssistantMessageArea";
200
+
201
+ interface AssistantInputProps
202
+ extends React.FormHTMLAttributes<HTMLFormElement> {
203
+ sendMessageFunc: (event: React.FormEvent<HTMLFormElement>) => Promise<void>;
204
+ isButtonDisabled: boolean;
205
+ input: string;
206
+ handleInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
207
+ isMessageStreaming: boolean;
208
+ }
209
+
210
+ const AssistantInput = React.forwardRef<HTMLFormElement, AssistantInputProps>(
211
+ (
212
+ {
213
+ className,
214
+ sendMessageFunc,
215
+ isButtonDisabled,
216
+ input,
217
+ handleInputChange,
218
+ isMessageStreaming,
219
+ ...props
220
+ },
221
+ ref
222
+ ) => {
223
+ return (
224
+ <form
225
+ onSubmit={sendMessageFunc}
226
+ className={cn("flex items-center space-x-4", className)}
227
+ ref={ref}
228
+ {...props}
229
+ >
230
+ <input
231
+ type="text"
232
+ disabled={isButtonDisabled}
233
+ value={input}
234
+ onChange={handleInputChange}
235
+ placeholder={
236
+ isMessageStreaming ? "Procesando..." : "Escribe tu mensaje aquí..."
237
+ }
238
+ className={cn(
239
+ "flex-1 text-sm p-3 rounded-lg focus:outline-none w-full",
240
+ "border border-input bg-background text-foreground",
241
+ "placeholder:text-muted-foreground",
242
+ "focus:ring-2 focus:ring-ring",
243
+ "disabled:opacity-50 disabled:cursor-not-allowed"
244
+ )}
245
+ />
246
+ <button
247
+ type="submit"
248
+ disabled={isButtonDisabled}
249
+ className={cn(
250
+ "w-10 h-10 rounded-full flex items-center justify-center alq--assistant-button-send",
251
+ isButtonDisabled
252
+ ? "bg-muted text-muted-foreground"
253
+ : "bg-primary text-primary-foreground hover:bg-primary/90"
254
+ )}
255
+ >
256
+ <Send className="w-5 h-5" />
257
+ </button>
258
+ </form>
259
+ );
260
+ }
261
+ );
262
+ AssistantInput.displayName = "AssistantInput";
263
+
264
+ export {
265
+ Assistant,
266
+ AssistantTitle,
267
+ AssistantDescription,
268
+ AssistantHeader,
269
+ AssistantMessageArea,
270
+ AssistantInput,
271
+ };
@@ -0,0 +1,6 @@
1
+ export { Whisper } from "./whisper";
2
+ export { SpeechToText } from "./speechToText";
3
+ export { Assistant } from "./assistant";
4
+ export { RatingDialog } from "./rating-dialog";
5
+ export { AssistantMessageArea } from "./assistant";
6
+ export { AssistantInput } from "./assistant";
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { Message } from "ai";
4
+
5
+ import { RatingData } from "../../types/type";
6
+ import { useRatings } from "@alquimia-ai/tools/hooks";
7
+ import { useToast } from "../hooks";
8
+ import { RatingThumbs, RatingStars, RatingComment } from "../molecules";
9
+ import {
10
+ Dialog,
11
+ DialogDescription,
12
+ DialogTitle,
13
+ DialogContent,
14
+ DialogHeader,
15
+ Typography,
16
+ Button,
17
+ ToastAction,
18
+ } from "../atoms";
19
+
20
+ interface RatingDialogProps {
21
+ topicId: string;
22
+ assistantId: string;
23
+ sendRating: (ratingData: RatingData) => Promise<any>;
24
+ isOpen: boolean;
25
+ onOpenChange: (open: boolean) => void;
26
+ setHasRatedTopic: (hasRated: boolean) => void;
27
+ onError?: (componentName: string, error: Error) => void;
28
+ }
29
+
30
+ export function RatingDialog({
31
+ sendRating,
32
+ topicId,
33
+ assistantId,
34
+ isOpen,
35
+ onOpenChange,
36
+ setHasRatedTopic,
37
+ }: RatingDialogProps) {
38
+ const { toast } = useToast();
39
+ const { handleRate, ratingStars, ratingThumbs, ratingComment, isLoading } =
40
+ useRatings({ assistantId, sendRating, topicId });
41
+
42
+ const onHandleRateSuccess = (success: boolean, err?: string) => {
43
+ setTimeout(() => onOpenChange(false), 1000);
44
+ success
45
+ ? setHasRatedTopic(true)
46
+ : toast({
47
+ title: "Error",
48
+ description: `Hubo un error al enviar la calificación`,
49
+ action: (
50
+ <ToastAction altText="Goto schedule to undo">Cerrar</ToastAction>
51
+ ),
52
+ });
53
+ };
54
+
55
+ const handleCancelRate = () => {
56
+ setHasRatedTopic(true);
57
+ onOpenChange(false);
58
+ };
59
+
60
+ return (
61
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
62
+ <DialogContent className="sm:max-w-md">
63
+ <DialogHeader>
64
+ <DialogTitle>Calificá esta conversación</DialogTitle>
65
+ <DialogDescription>
66
+ Por favor, compartí tu opinión sobre el asistente.
67
+ </DialogDescription>
68
+ </DialogHeader>
69
+ <div className="flex flex-row justify-center gap-4 py-4">
70
+ <RatingStars
71
+ currentRating={ratingStars}
72
+ onRate={(newRating) =>
73
+ handleRate("score", newRating, onHandleRateSuccess)
74
+ }
75
+ isLoading={isLoading}
76
+ className="p-2 rounded"
77
+ />
78
+ <RatingThumbs
79
+ currentRating={ratingComment}
80
+ onRate={(newRating) =>
81
+ handleRate("description", newRating, onHandleRateSuccess)
82
+ }
83
+ direction="row"
84
+ isLoading={isLoading}
85
+ />
86
+ <RatingComment
87
+ currentRating={ratingComment}
88
+ onRate={(newRating) =>
89
+ handleRate("description", newRating, onHandleRateSuccess)
90
+ }
91
+ isLoading={isLoading}
92
+ />
93
+ </div>
94
+ <div className="flex flex-row justify-center gap-4">
95
+ <Button onClick={handleCancelRate} variant="secondary">
96
+ <Typography as="p" typeStyle="tiny">
97
+ No, gracias.
98
+ </Typography>
99
+ </Button>
100
+ </div>
101
+ </DialogContent>
102
+ </Dialog>
103
+ );
104
+ }
@@ -0,0 +1,92 @@
1
+ import { useState, useEffect } from "react";
2
+ import { cn, blobToBase64 } from "../../lib/utils";
3
+ interface SpeechToTextProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ RecordAudioIcon: React.ReactElement;
5
+ IdleAudioIcon: React.ReactElement;
6
+ speechToText: (audio: string) => Promise<string>;
7
+ handleReplaceInput: (text: string) => void;
8
+ setIsAudioRecording: React.Dispatch<React.SetStateAction<boolean>>;
9
+ }
10
+
11
+ function SpeechToText({
12
+ className,
13
+ speechToText,
14
+ RecordAudioIcon,
15
+ IdleAudioIcon,
16
+ handleReplaceInput,
17
+ setIsAudioRecording,
18
+ ...props
19
+ }: SpeechToTextProps) {
20
+ const [isRecording, setIsRecording] = useState<boolean>(false);
21
+ const [isLoading, setIsLoading] = useState<boolean>(false);
22
+ const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
23
+ const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
24
+
25
+ const startRecording = async () => {
26
+ setIsRecording(true);
27
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
28
+ const recorder = new MediaRecorder(stream);
29
+ setMediaRecorder(recorder);
30
+
31
+ recorder.ondataavailable = (event) => {
32
+ setAudioBlob(event.data);
33
+ };
34
+
35
+ recorder.start();
36
+ };
37
+
38
+ const stopRecording = () => {
39
+ setIsRecording(false);
40
+ mediaRecorder?.stop();
41
+ mediaRecorder?.stream.getTracks().forEach(track => track.stop());
42
+ };
43
+
44
+ const handleSpeechToText = async () => {
45
+ if (!audioBlob) return;
46
+
47
+ const audioBase64 = await blobToBase64(audioBlob);
48
+ try {
49
+ setIsLoading(true)
50
+ const transcription = await speechToText(audioBase64);
51
+ if (transcription) {
52
+ handleReplaceInput(transcription)
53
+ setIsLoading(false)
54
+ }
55
+ } catch (error) {
56
+ console.error('Error transcribing audio:', error);
57
+ setIsLoading(false)
58
+ } finally {
59
+ setAudioBlob(null);
60
+ }
61
+ };
62
+
63
+ useEffect(() => {
64
+ if (!isRecording && audioBlob) {
65
+ handleSpeechToText();
66
+ }
67
+ }, [isRecording, audioBlob]);
68
+
69
+ useEffect(() => {
70
+ isLoading || isRecording ? setIsAudioRecording(true) : setIsAudioRecording(false);
71
+ }, [isRecording, isLoading]);
72
+
73
+ const handleRecordButtonClick = () => {
74
+ if (isRecording) {
75
+ stopRecording();
76
+ } else {
77
+ startRecording();
78
+ }
79
+ };
80
+
81
+ const recordIcons = isRecording ? RecordAudioIcon : IdleAudioIcon
82
+
83
+ return (
84
+ <div className={cn(className)} {...props}>
85
+ <button disabled={isLoading} type="button" onClick={handleRecordButtonClick}>
86
+ {recordIcons}
87
+ </button>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ export { SpeechToText };