@terreno/ui 0.2.0 → 0.3.0

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.
@@ -0,0 +1,147 @@
1
+ import React from "react";
2
+
3
+ import {Box} from "./Box";
4
+ import type {DataTableColumn} from "./Common";
5
+ import {DataTable} from "./DataTable";
6
+ import {DateTimeField} from "./DateTimeField";
7
+ import {Heading} from "./Heading";
8
+ import {MultiselectField} from "./MultiselectField";
9
+ import {Pagination} from "./Pagination";
10
+ import {Spinner} from "./Spinner";
11
+ import {Text} from "./Text";
12
+
13
+ export interface AIRequestExplorerData {
14
+ aiModel: string;
15
+ created: string;
16
+ error?: string;
17
+ prompt: string;
18
+ requestType: string;
19
+ response?: string;
20
+ responseTime?: number;
21
+ tokensUsed?: number;
22
+ user?: {email?: string; name?: string};
23
+ }
24
+
25
+ export interface AIRequestExplorerProps {
26
+ data: AIRequestExplorerData[];
27
+ endDate?: string;
28
+ isLoading?: boolean;
29
+ onEndDateChange?: (date: string) => void;
30
+ onPageChange: (page: number) => void;
31
+ onRequestTypeFilterChange?: (types: string[]) => void;
32
+ onStartDateChange?: (date: string) => void;
33
+ page: number;
34
+ requestTypeFilter?: string[];
35
+ startDate?: string;
36
+ testID?: string;
37
+ totalCount: number;
38
+ totalPages: number;
39
+ }
40
+
41
+ const REQUEST_TYPE_OPTIONS = [
42
+ {label: "General", value: "general"},
43
+ {label: "Remix", value: "remix"},
44
+ {label: "Summarization", value: "summarization"},
45
+ {label: "Translation", value: "translation"},
46
+ ];
47
+
48
+ const COLUMNS: DataTableColumn[] = [
49
+ {columnType: "text", title: "Type", width: 120},
50
+ {columnType: "text", title: "Model", width: 150},
51
+ {columnType: "text", title: "User", width: 150},
52
+ {columnType: "text", title: "Prompt", width: 250},
53
+ {columnType: "text", title: "Response", width: 250},
54
+ {columnType: "number", title: "Tokens", width: 80},
55
+ {columnType: "text", title: "Time (ms)", width: 100},
56
+ {columnType: "text", title: "Created", width: 180},
57
+ {columnType: "text", title: "Error", width: 150},
58
+ ];
59
+
60
+ const formatRow = (item: AIRequestExplorerData) => {
61
+ return [
62
+ {value: item.requestType},
63
+ {value: item.aiModel},
64
+ {value: item.user?.name ?? item.user?.email ?? "-"},
65
+ {value: item.prompt ?? "-"},
66
+ {value: item.response ?? "-"},
67
+ {value: item.tokensUsed?.toString() ?? "-"},
68
+ {value: item.responseTime != null ? `${item.responseTime}ms` : "-"},
69
+ {value: item.created ? new Date(item.created).toLocaleString() : "-"},
70
+ {value: item.error ?? ""},
71
+ ];
72
+ };
73
+
74
+ export const AIRequestExplorer = ({
75
+ data,
76
+ endDate,
77
+ isLoading = false,
78
+ onEndDateChange,
79
+ onPageChange,
80
+ onRequestTypeFilterChange,
81
+ onStartDateChange,
82
+ page,
83
+ requestTypeFilter,
84
+ startDate,
85
+ testID,
86
+ totalCount,
87
+ totalPages,
88
+ }: AIRequestExplorerProps): React.ReactElement => {
89
+ return (
90
+ <Box direction="column" flex="grow" gap={4} padding={4} testID={testID}>
91
+ <Heading size="lg">AI Request Explorer</Heading>
92
+ <Text color="secondaryDark" size="sm">
93
+ {totalCount} total requests
94
+ </Text>
95
+
96
+ {/* Filters */}
97
+ <Box direction="row" gap={3} wrap>
98
+ {onRequestTypeFilterChange ? (
99
+ <Box minWidth={200}>
100
+ <MultiselectField
101
+ onChange={onRequestTypeFilterChange}
102
+ options={REQUEST_TYPE_OPTIONS}
103
+ title="Request Type"
104
+ value={requestTypeFilter ?? []}
105
+ />
106
+ </Box>
107
+ ) : null}
108
+ {onStartDateChange ? (
109
+ <Box minWidth={200}>
110
+ <DateTimeField
111
+ onChange={onStartDateChange}
112
+ title="Start Date"
113
+ type="datetime"
114
+ value={startDate ?? ""}
115
+ />
116
+ </Box>
117
+ ) : null}
118
+ {onEndDateChange ? (
119
+ <Box minWidth={200}>
120
+ <DateTimeField
121
+ onChange={onEndDateChange}
122
+ title="End Date"
123
+ type="datetime"
124
+ value={endDate ?? ""}
125
+ />
126
+ </Box>
127
+ ) : null}
128
+ </Box>
129
+
130
+ {/* Table */}
131
+ {isLoading ? (
132
+ <Box alignItems="center" padding={6}>
133
+ <Spinner />
134
+ </Box>
135
+ ) : (
136
+ <DataTable alternateRowBackground columns={COLUMNS} data={data.map(formatRow)} />
137
+ )}
138
+
139
+ {/* Pagination */}
140
+ {totalPages > 1 ? (
141
+ <Box alignItems="center">
142
+ <Pagination page={page} setPage={onPageChange} totalPages={totalPages} />
143
+ </Box>
144
+ ) : null}
145
+ </Box>
146
+ );
147
+ };
@@ -0,0 +1,63 @@
1
+ import React from "react";
2
+ import {Image as RNImage} from "react-native";
3
+
4
+ import {Box} from "./Box";
5
+ import {DismissButton} from "./DismissButton";
6
+ import type {SelectedFile} from "./FilePickerButton";
7
+ import {Icon} from "./Icon";
8
+ import {Text} from "./Text";
9
+
10
+ export interface AttachmentPreviewProps {
11
+ attachments: SelectedFile[];
12
+ onRemove: (index: number) => void;
13
+ testID?: string;
14
+ }
15
+
16
+ const isImageMimeType = (mimeType: string): boolean => {
17
+ return mimeType.startsWith("image/");
18
+ };
19
+
20
+ export const AttachmentPreview = ({
21
+ attachments,
22
+ onRemove,
23
+ testID,
24
+ }: AttachmentPreviewProps): React.ReactElement | null => {
25
+ if (attachments.length === 0) {
26
+ return null;
27
+ }
28
+
29
+ return (
30
+ <Box direction="row" gap={2} padding={2} testID={testID ?? "attachment-preview"} wrap>
31
+ {attachments.map((attachment, index) => (
32
+ <Box
33
+ alignItems="center"
34
+ border="default"
35
+ direction="row"
36
+ gap={1}
37
+ key={`attachment-${index}`}
38
+ padding={1}
39
+ rounding="md"
40
+ >
41
+ {isImageMimeType(attachment.mimeType) ? (
42
+ <RNImage
43
+ source={{uri: attachment.uri}}
44
+ style={{borderRadius: 4, height: 40, width: 40}}
45
+ />
46
+ ) : (
47
+ <Box alignItems="center" justifyContent="center" padding={1}>
48
+ <Icon iconName="file" size="sm" />
49
+ </Box>
50
+ )}
51
+ <Text size="sm" truncate>
52
+ {attachment.name}
53
+ </Text>
54
+ <DismissButton
55
+ accessibilityHint="Removes this attachment"
56
+ accessibilityLabel={`Remove ${attachment.name}`}
57
+ onClick={() => onRemove(index)}
58
+ />
59
+ </Box>
60
+ ))}
61
+ </Box>
62
+ );
63
+ };
@@ -0,0 +1,88 @@
1
+ import React, {useCallback, useState} from "react";
2
+
3
+ import {Box} from "./Box";
4
+ import {Button} from "./Button";
5
+ import {IconButton} from "./IconButton";
6
+ import {Modal} from "./Modal";
7
+
8
+ export interface SelectedFile {
9
+ mimeType: string;
10
+ name: string;
11
+ uri: string;
12
+ }
13
+
14
+ export interface FilePickerButtonProps {
15
+ disabled?: boolean;
16
+ multiple?: boolean;
17
+ onFilesSelected: (files: SelectedFile[]) => void;
18
+ testID?: string;
19
+ }
20
+
21
+ export const FilePickerButton = ({
22
+ disabled = false,
23
+ multiple = false,
24
+ onFilesSelected,
25
+ testID,
26
+ }: FilePickerButtonProps): React.ReactElement => {
27
+ const [showModal, setShowModal] = useState(false);
28
+
29
+ const handlePickImage = useCallback(async () => {
30
+ setShowModal(false);
31
+ const ImagePicker = await import("expo-image-picker");
32
+ const result = await ImagePicker.launchImageLibraryAsync({
33
+ allowsMultipleSelection: multiple,
34
+ mediaTypes: ["images"],
35
+ quality: 0.8,
36
+ });
37
+
38
+ if (!result.canceled && result.assets.length > 0) {
39
+ const files: SelectedFile[] = result.assets.map((asset) => ({
40
+ mimeType: asset.mimeType ?? "image/jpeg",
41
+ name: asset.fileName ?? `image-${Date.now()}.jpg`,
42
+ uri: asset.uri,
43
+ }));
44
+ onFilesSelected(files);
45
+ }
46
+ }, [multiple, onFilesSelected]);
47
+
48
+ const handlePickDocument = useCallback(async () => {
49
+ setShowModal(false);
50
+ const DocumentPicker = await import("expo-document-picker");
51
+ const result = await DocumentPicker.getDocumentAsync({
52
+ multiple,
53
+ type: ["application/pdf", "text/plain", "text/csv", "application/json"],
54
+ });
55
+
56
+ if (!result.canceled && result.assets.length > 0) {
57
+ const files: SelectedFile[] = result.assets.map((asset) => ({
58
+ mimeType: asset.mimeType ?? "application/octet-stream",
59
+ name: asset.name,
60
+ uri: asset.uri,
61
+ }));
62
+ onFilesSelected(files);
63
+ }
64
+ }, [multiple, onFilesSelected]);
65
+
66
+ return (
67
+ <>
68
+ <IconButton
69
+ accessibilityLabel="Attach file"
70
+ disabled={disabled}
71
+ iconName="paperclip"
72
+ onClick={() => setShowModal(true)}
73
+ testID={testID ?? "file-picker-button"}
74
+ />
75
+ <Modal onDismiss={() => setShowModal(false)} size="sm" title="Attach" visible={showModal}>
76
+ <Box gap={2} padding={3}>
77
+ <Button
78
+ iconName="image"
79
+ onClick={handlePickImage}
80
+ text="Photo Library"
81
+ variant="outline"
82
+ />
83
+ <Button iconName="file" onClick={handlePickDocument} text="Document" variant="outline" />
84
+ </Box>
85
+ </Modal>
86
+ </>
87
+ );
88
+ };