@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.
- package/dist/AIRequestExplorer.d.ts +31 -0
- package/dist/AIRequestExplorer.js +44 -0
- package/dist/AIRequestExplorer.js.map +1 -0
- package/dist/AttachmentPreview.d.ts +8 -0
- package/dist/AttachmentPreview.js +16 -0
- package/dist/AttachmentPreview.js.map +1 -0
- package/dist/FilePickerButton.d.ts +13 -0
- package/dist/FilePickerButton.js +50 -0
- package/dist/FilePickerButton.js.map +1 -0
- package/dist/GPTChat.d.ts +66 -0
- package/dist/GPTChat.js +112 -0
- package/dist/GPTChat.js.map +1 -0
- package/dist/GPTMemoryModal.d.ts +8 -0
- package/dist/GPTMemoryModal.js +14 -0
- package/dist/GPTMemoryModal.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/AIRequestExplorer.tsx +147 -0
- package/src/AttachmentPreview.tsx +63 -0
- package/src/FilePickerButton.tsx +88 -0
- package/src/GPTChat.tsx +551 -0
- package/src/GPTMemoryModal.tsx +50 -0
- package/src/index.tsx +5 -0
|
@@ -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
|
+
};
|