Merge: branch main from upstream

This commit is contained in:
ManishMadan2882
2024-09-11 23:12:15 +05:30
32 changed files with 1622 additions and 165 deletions

View File

@@ -12,6 +12,10 @@ const endpoints = {
SINGLE_PROMPT: (id: string) => `/api/get_single_prompt?id=${id}`,
DELETE_PATH: (docPath: string) => `/api/delete_old?source_id=${docPath}`,
TASK_STATUS: (task_id: string) => `/api/task_status?task_id=${task_id}`,
MESSAGE_ANALYTICS: '/api/get_message_analytics',
TOKEN_ANALYTICS: '/api/get_token_analytics',
FEEDBACK_ANALYTICS: '/api/get_feedback_analytics',
LOGS: `/api/get_user_logs`,
},
CONVERSATION: {
ANSWER: '/api/answer',

View File

@@ -23,6 +23,14 @@ const userService = {
apiClient.get(endpoints.USER.DELETE_PATH(docPath)),
getTaskStatus: (task_id: string): Promise<any> =>
apiClient.get(endpoints.USER.TASK_STATUS(task_id)),
getMessageAnalytics: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.MESSAGE_ANALYTICS, data),
getTokenAnalytics: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.TOKEN_ANALYTICS, data),
getFeedbackAnalytics: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.FEEDBACK_ANALYTICS, data),
getLogs: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.LOGS, data),
};
export default userService;

View File

@@ -0,0 +1,3 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.29154 4.88202L1.70154 0.29202C1.60896 0.199438 1.49905 0.125998 1.37808 0.0758932C1.25712 0.0257882 1.12747 -2.37536e-07 0.99654 -2.44235e-07C0.86561 -2.50934e-07 0.735961 0.0257882 0.614997 0.0758931C0.494033 0.125998 0.384122 0.199438 0.29154 0.29202C0.198958 0.384602 0.125519 0.494513 0.0754137 0.615477C0.0253086 0.736441 -0.00048069 0.86609 -0.000480695 0.99702C-0.000480701 1.12795 0.0253086 1.2576 0.0754136 1.37856C0.125519 1.49953 0.198958 1.60944 0.29154 1.70202L4.17154 5.59202L0.29154 9.47202C0.198958 9.5646 0.125518 9.67451 0.0754133 9.79548C0.0253082 9.91644 -0.000481091 10.0461 -0.000481097 10.177C-0.000481102 10.3079 0.0253082 10.4376 0.0754132 10.5586C0.125518 10.6795 0.198958 10.7894 0.29154 10.882C0.384121 10.9746 0.494032 11.048 0.614996 11.0981C0.73596 11.1483 0.865609 11.174 0.99654 11.174C1.12747 11.174 1.25712 11.1483 1.37808 11.0981C1.49905 11.048 1.60896 10.9746 1.70154 10.882L6.29154 6.29202C6.38424 6.19951 6.45779 6.08962 6.50797 5.96864C6.55815 5.84767 6.58398 5.71799 6.58398 5.58702C6.58398 5.45605 6.55815 5.32637 6.50797 5.2054C6.45779 5.08442 6.38424 4.97453 6.29154 4.88202Z" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,16 +1,24 @@
import { useState } from 'react';
import Copy from './../assets/copy.svg?react';
import CheckMark from './../assets/checkmark.svg?react';
import copy from 'copy-to-clipboard';
import { useState } from 'react';
export default function CoppyButton({ text }: { text: string }) {
import CheckMark from '../assets/checkmark.svg?react';
import Copy from '../assets/copy.svg?react';
export default function CoppyButton({
text,
colorLight,
colorDark,
}: {
text: string;
colorLight?: string;
colorDark?: string;
}) {
const [copied, setCopied] = useState(false);
const [isCopyHovered, setIsCopyHovered] = useState(false);
const handleCopyClick = (text: string) => {
copy(text);
setCopied(true);
// Reset copied to false after a few seconds
setTimeout(() => {
setCopied(false);
}, 3000);
@@ -20,8 +28,8 @@ export default function CoppyButton({ text }: { text: string }) {
<div
className={`flex items-center justify-center rounded-full p-2 ${
isCopyHovered
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
: 'bg-[#ffffff] dark:bg-transparent'
? `bg-[#EEEEEE] dark:bg-purple-taupe`
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
}`}
>
{copied ? (

View File

@@ -16,6 +16,7 @@ function Dropdown({
showDelete,
onDelete,
placeholder,
contentSize = 'text-base',
}: {
options:
| string[]
@@ -42,6 +43,7 @@ function Dropdown({
showDelete?: boolean;
onDelete?: (value: string) => void;
placeholder?: string;
contentSize?: string;
}) {
const dropdownRef = React.useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = React.useState(false);
@@ -85,9 +87,9 @@ function Dropdown({
</span>
) : (
<span
className={`overflow-hidden text-ellipsis dark:text-bright-gray ${
className={`truncate overflow-hidden dark:text-bright-gray ${
!selectedValue && 'text-silver dark:text-gray-400'
}`}
} ${contentSize}`}
>
{selectedValue && 'label' in selectedValue
? selectedValue.label
@@ -124,7 +126,7 @@ function Dropdown({
onSelect(option);
setIsOpen(false);
}}
className="ml-5 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3 dark:text-light-gray"
className={`ml-5 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3 dark:text-light-gray ${contentSize}`}
>
{typeof option === 'string'
? option

View File

@@ -10,6 +10,7 @@ const Input = ({
maxLength,
className,
colorVariant = 'silver',
borderVariant = 'thick',
children,
onChange,
onPaste,
@@ -20,10 +21,13 @@ const Input = ({
jet: 'border-jet',
gray: 'border-gray-5000 dark:text-silver',
};
const borderStyles = {
thin: 'border',
thick: 'border-2',
};
return (
<input
className={`h-[42px] w-full rounded-full border-2 px-3 outline-none dark:bg-transparent dark:text-white ${className} ${colorStyles[colorVariant]}`}
className={`h-[42px] w-full rounded-full px-3 py-1 outline-none dark:bg-transparent dark:text-white ${className} ${colorStyles[colorVariant]} ${borderStyles[borderVariant]}`}
type={type}
id={id}
name={name}

View File

@@ -2,6 +2,7 @@ export type InputProps = {
type: 'text' | 'number';
value: string | string[] | number;
colorVariant?: 'silver' | 'jet' | 'gray';
borderVariant?: 'thin' | 'thick';
isAutoFocused?: boolean;
id?: string;
maxLength?: number;

View File

@@ -180,7 +180,7 @@ const ConversationBubble = forwardRef<
))}
{(sources?.length ?? 0) > 3 && (
<div
className="flex h-24 cursor-pointer flex-col-reverse rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
className="flex h-28 cursor-pointer flex-col-reverse rounded-[20px] bg-gray-1000 p-4 text-purple-30 hover:bg-[#F1F1F1] hover:text-[#6D3ECC] dark:bg-gun-metal dark:hover:bg-[#2C2E3C] dark:hover:text-[#8C67D7]"
onClick={() => setIsSidebarOpen(true)}
>
<p className="ellipsis-text h-22 text-xs">{`View ${

View File

@@ -420,10 +420,36 @@ template {
src: url('/fonts/Inter-Variable.ttf');
}
@font-face {
font-family: 'IBMPlexMono-Medium';
font-weight: 500;
src: url('/fonts/IBMPlexMono-Medium.ttf');
}
::-webkit-scrollbar {
width: 0;
}
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0 50px white inset;
}
input:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 50px white inset;
}
@media (prefers-color-scheme: dark) {
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0 50px rgb(68, 70, 84) inset;
-webkit-text-fill-color: white;
}
input:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 50px rgb(68, 70, 84) inset;
-webkit-text-fill-color: white;
}
}
.inputbox-style {
resize: none;
padding-left: 36px;
@@ -441,3 +467,7 @@ template {
-webkit-box-orient: vertical;
text-overflow: ellipsis;
}
.logs-table {
font-family: 'IBMPlexMono-Medium', system-ui;
}

View File

@@ -62,6 +62,12 @@
"key": "API Key",
"sourceDoc": "Source Document",
"createNew": "Create New"
},
"analytics": {
"label": "Analytics"
},
"logs": {
"label": "Logs"
}
},
"modals": {

View File

@@ -62,6 +62,12 @@
"key": "Clave de API",
"sourceDoc": "Documento Fuente",
"createNew": "Crear Nuevo"
},
"analytics": {
"label": "Analítica"
},
"logs": {
"label": "Registros"
}
},
"modals": {

View File

@@ -62,6 +62,12 @@
"key": "APIキー",
"sourceDoc": "ソースドキュメント",
"createNew": "新規作成"
},
"analytics": {
"label": "分析"
},
"logs": {
"label": "ログ"
}
},
"modals": {

View File

@@ -62,6 +62,12 @@
"key": "API 密钥",
"sourceDoc": "源文档",
"createNew": "创建新的"
},
"analytics": {
"label": "分析"
},
"logs": {
"label": "日志"
}
},
"modals": {

View File

@@ -5,15 +5,14 @@ import userService from '../api/services/userService';
import Trash from '../assets/trash.svg';
import CreateAPIKeyModal from '../modals/CreateAPIKeyModal';
import SaveAPIKeyModal from '../modals/SaveAPIKeyModal';
import { APIKeyData } from './types';
export default function APIKeys() {
const { t } = useTranslation();
const [isCreateModalOpen, setCreateModal] = React.useState(false);
const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false);
const [newKey, setNewKey] = React.useState('');
const [apiKeys, setApiKeys] = React.useState<
{ name: string; key: string; source: string; id: string }[]
>([]);
const [apiKeys, setApiKeys] = React.useState<APIKeyData[]>([]);
const handleFetchKeys = async () => {
try {

View File

@@ -0,0 +1,390 @@
import {
BarElement,
CategoryScale,
Chart as ChartJS,
Legend,
LinearScale,
Title,
Tooltip,
} from 'chart.js';
import React from 'react';
import { Bar } from 'react-chartjs-2';
import userService from '../api/services/userService';
import Dropdown from '../components/Dropdown';
import { htmlLegendPlugin } from '../utils/chartUtils';
import { formatDate } from '../utils/dateTimeUtils';
import { APIKeyData } from './types';
import type { ChartData } from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
);
const filterOptions = [
{ label: 'Hour', value: 'last_hour' },
{ label: '24 Hours', value: 'last_24_hour' },
{ label: '7 Days', value: 'last_7_days' },
{ label: '15 Days', value: 'last_15_days' },
{ label: '30 Days', value: 'last_30_days' },
];
export default function Analytics() {
const [messagesData, setMessagesData] = React.useState<Record<
string,
number
> | null>(null);
const [tokenUsageData, setTokenUsageData] = React.useState<Record<
string,
number
> | null>(null);
const [feedbackData, setFeedbackData] = React.useState<Record<
string,
{
positive: number;
negative: number;
}
> | null>(null);
const [chatbots, setChatbots] = React.useState<APIKeyData[]>([]);
const [selectedChatbot, setSelectedChatbot] =
React.useState<APIKeyData | null>();
const [messagesFilter, setMessagesFilter] = React.useState<{
label: string;
value: string;
}>({ label: '30 Days', value: 'last_30_days' });
const [tokenUsageFilter, setTokenUsageFilter] = React.useState<{
label: string;
value: string;
}>({ label: '30 Days', value: 'last_30_days' });
const [feedbackFilter, setFeedbackFilter] = React.useState<{
label: string;
value: string;
}>({ label: '30 Days', value: 'last_30_days' });
const fetchChatbots = async () => {
try {
const response = await userService.getAPIKeys();
if (!response.ok) {
throw new Error('Failed to fetch Chatbots');
}
const chatbots = await response.json();
setChatbots(chatbots);
} catch (error) {
console.error(error);
}
};
const fetchMessagesData = async (chatbot_id?: string, filter?: string) => {
try {
const response = await userService.getMessageAnalytics({
api_key_id: chatbot_id,
filter_option: filter,
});
if (!response.ok) {
throw new Error('Failed to fetch analytics data');
}
const data = await response.json();
setMessagesData(data.messages);
} catch (error) {
console.error(error);
}
};
const fetchTokenData = async (chatbot_id?: string, filter?: string) => {
try {
const response = await userService.getTokenAnalytics({
api_key_id: chatbot_id,
filter_option: filter,
});
if (!response.ok) {
throw new Error('Failed to fetch analytics data');
}
const data = await response.json();
setTokenUsageData(data.token_usage);
} catch (error) {
console.error(error);
}
};
const fetchFeedbackData = async (chatbot_id?: string, filter?: string) => {
try {
const response = await userService.getFeedbackAnalytics({
api_key_id: chatbot_id,
filter_option: filter,
});
if (!response.ok) {
throw new Error('Failed to fetch analytics data');
}
const data = await response.json();
setFeedbackData(data.feedback);
} catch (error) {
console.error(error);
}
};
React.useEffect(() => {
fetchChatbots();
}, []);
React.useEffect(() => {
const id = selectedChatbot?.id;
const filter = messagesFilter;
fetchMessagesData(id, filter?.value);
}, [selectedChatbot, messagesFilter]);
React.useEffect(() => {
const id = selectedChatbot?.id;
const filter = tokenUsageFilter;
fetchTokenData(id, filter?.value);
}, [selectedChatbot, tokenUsageFilter]);
React.useEffect(() => {
const id = selectedChatbot?.id;
const filter = feedbackFilter;
fetchFeedbackData(id, filter?.value);
}, [selectedChatbot, feedbackFilter]);
return (
<div className="mt-12">
<div className="flex flex-col items-start">
<div className="flex flex-col gap-3">
<p className="font-bold text-jet dark:text-bright-gray">
Filter by chatbot
</p>
<Dropdown
size="w-[55vw] sm:w-[360px]"
options={[
...chatbots.map((chatbot) => ({
label: chatbot.name,
value: chatbot.id,
})),
{ label: 'None', value: '' },
]}
placeholder="Select chatbot"
onSelect={(chatbot: { label: string; value: string }) => {
setSelectedChatbot(
chatbots.find((item) => item.id === chatbot.value),
);
}}
selectedValue={
(selectedChatbot && {
label: selectedChatbot.name,
value: selectedChatbot.id,
}) ||
null
}
rounded="3xl"
border="border"
/>
</div>
<div className="mt-8 w-full flex flex-col sm:flex-row gap-3">
<div className="h-[345px] sm:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
<div className="flex flex-row items-center justify-start gap-3">
<p className="font-bold text-jet dark:text-bright-gray">
Messages
</p>
<Dropdown
size="w-[125px]"
options={filterOptions}
placeholder="Filter"
onSelect={(selectedOption: {
label: string;
value: string;
}) => {
setMessagesFilter(selectedOption);
}}
selectedValue={messagesFilter ?? null}
rounded="3xl"
border="border"
contentSize="text-sm"
/>
</div>
<div className="mt-px relative h-[245px] w-full">
<div
id="legend-container-1"
className="flex flex-row items-center justify-end"
></div>
<AnalyticsChart
data={{
labels: Object.keys(messagesData || {}).map((item) =>
formatDate(item),
),
datasets: [
{
label: 'Messages',
data: Object.values(messagesData || {}),
backgroundColor: '#7D54D1',
},
],
}}
legendID="legend-container-1"
maxTicksLimitInX={8}
isStacked={false}
/>
</div>
</div>
<div className="h-[345px] sm:w-1/2 w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
<div className="flex flex-row items-center justify-start gap-3">
<p className="font-bold text-jet dark:text-bright-gray">
Token Usage
</p>
<Dropdown
size="w-[125px]"
options={filterOptions}
placeholder="Filter"
onSelect={(selectedOption: {
label: string;
value: string;
}) => {
setTokenUsageFilter(selectedOption);
}}
selectedValue={tokenUsageFilter ?? null}
rounded="3xl"
border="border"
contentSize="text-sm"
/>
</div>
<div className="mt-px relative h-[245px] w-full">
<div
id="legend-container-2"
className="flex flex-row items-center justify-end"
></div>
<AnalyticsChart
data={{
labels: Object.keys(tokenUsageData || {}).map((item) =>
formatDate(item),
),
datasets: [
{
label: 'Tokens',
data: Object.values(tokenUsageData || {}),
backgroundColor: '#7D54D1',
},
],
}}
legendID="legend-container-2"
maxTicksLimitInX={8}
isStacked={false}
/>
</div>
</div>
</div>
<div className="mt-8 w-full">
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40">
<div className="flex flex-row items-center justify-start gap-3">
<p className="font-bold text-jet dark:text-bright-gray">
User Feedback
</p>
<Dropdown
size="w-[125px]"
options={filterOptions}
placeholder="Filter"
onSelect={(selectedOption: {
label: string;
value: string;
}) => {
setFeedbackFilter(selectedOption);
}}
selectedValue={feedbackFilter ?? null}
rounded="3xl"
border="border"
contentSize="text-sm"
/>
</div>
<div className="mt-px relative h-[245px] w-full">
<div
id="legend-container-3"
className="flex flex-row items-center justify-end"
></div>
<AnalyticsChart
data={{
labels: Object.keys(feedbackData || {}).map((item) =>
formatDate(item),
),
datasets: [
{
label: 'Positive',
data: Object.values(feedbackData || {}).map(
(item) => item.positive,
),
backgroundColor: '#8BD154',
},
{
label: 'Negative',
data: Object.values(feedbackData || {}).map(
(item) => item.negative,
),
backgroundColor: '#D15454',
},
],
}}
legendID="legend-container-3"
maxTicksLimitInX={10}
isStacked={true}
/>
</div>
</div>
</div>
</div>
</div>
);
}
type AnalyticsChartProps = {
data: ChartData<'bar'>;
legendID: string;
maxTicksLimitInX: number;
isStacked: boolean;
};
function AnalyticsChart({
data,
legendID,
maxTicksLimitInX,
isStacked,
}: AnalyticsChartProps) {
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
htmlLegend: {
containerID: legendID,
},
},
scales: {
x: {
grid: {
lineWidth: 0.2,
color: '#C4C4C4',
},
border: {
width: 0.2,
color: '#C4C4C4',
},
ticks: {
maxTicksLimit: maxTicksLimitInX,
},
stacked: isStacked,
},
y: {
grid: {
lineWidth: 0.2,
color: '#C4C4C4',
},
border: {
width: 0.2,
color: '#C4C4C4',
},
stacked: isStacked,
},
},
};
return <Bar options={options} plugins={[htmlLegendPlugin]} data={data} />;
}

View File

@@ -89,7 +89,7 @@ export default function General() {
changeLanguage(selectedLanguage?.value);
}, [selectedLanguage, changeLanguage]);
return (
<div className="mt-[59px]">
<div className="mt-12">
<div className="mb-5">
<p className="font-bold text-jet dark:text-bright-gray">
{t('settings.general.selectTheme')}

View File

@@ -0,0 +1,175 @@
import React from 'react';
import userService from '../api/services/userService';
import ChevronRight from '../assets/chevron-right.svg';
import Dropdown from '../components/Dropdown';
import { APIKeyData, LogData } from './types';
import CoppyButton from '../components/CopyButton';
export default function Logs() {
const [chatbots, setChatbots] = React.useState<APIKeyData[]>([]);
const [selectedChatbot, setSelectedChatbot] =
React.useState<APIKeyData | null>();
const [logs, setLogs] = React.useState<LogData[]>([]);
const [page, setPage] = React.useState(1);
const [hasMore, setHasMore] = React.useState(true);
const fetchChatbots = async () => {
try {
const response = await userService.getAPIKeys();
if (!response.ok) {
throw new Error('Failed to fetch Chatbots');
}
const chatbots = await response.json();
setChatbots(chatbots);
} catch (error) {
console.error(error);
}
};
const fetchLogs = async () => {
try {
const response = await userService.getLogs({
page: page,
api_key_id: selectedChatbot?.id,
page_size: 10,
});
if (!response.ok) {
throw new Error('Failed to fetch logs');
}
const olderLogs = await response.json();
setLogs([...logs, ...olderLogs.logs]);
setHasMore(olderLogs.has_more);
} catch (error) {
console.error(error);
}
};
React.useEffect(() => {
fetchChatbots();
}, []);
React.useEffect(() => {
if (hasMore) fetchLogs();
}, [page, selectedChatbot]);
return (
<div className="mt-12">
<div className="flex flex-col items-start">
<div className="flex flex-col gap-3">
<p className="font-bold text-jet dark:text-bright-gray">
Filter by chatbot
</p>
<Dropdown
size="w-[55vw] sm:w-[360px]"
options={[
...chatbots.map((chatbot) => ({
label: chatbot.name,
value: chatbot.id,
})),
{ label: 'None', value: '' },
]}
placeholder="Select chatbot"
onSelect={(chatbot: { label: string; value: string }) => {
setSelectedChatbot(
chatbots.find((item) => item.id === chatbot.value),
);
setLogs([]);
setPage(1);
setHasMore(true);
}}
selectedValue={
(selectedChatbot && {
label: selectedChatbot.name,
value: selectedChatbot.id,
}) ||
null
}
rounded="3xl"
border="border"
/>
</div>
</div>
<div className="mt-8">
<LogsTable logs={logs} setPage={setPage} />
</div>
</div>
);
}
type LogsTableProps = {
logs: LogData[];
setPage: React.Dispatch<React.SetStateAction<number>>;
};
function LogsTable({ logs, setPage }: LogsTableProps) {
const observerRef = React.useRef<any>();
const firstObserver = React.useCallback((node: HTMLDivElement) => {
if (observerRef.current) {
observerRef.current = new IntersectionObserver((enteries) => {
if (enteries[0].isIntersecting) setPage((prev) => prev + 1);
});
}
if (node && observerRef.current) observerRef.current.observe(node);
}, []);
return (
<div className="logs-table border rounded-2xl h-[55vh] w-full overflow-hidden border-silver dark:border-silver/40">
<div className="h-8 bg-black/10 dark:bg-chinese-black flex flex-col items-start justify-center">
<p className="px-3 text-xs dark:text-gray-6000">
API generated / chatbot conversations
</p>
</div>
<div
ref={observerRef}
className="flex flex-col items-start h-[51vh] overflow-y-auto bg-transparent flex-grow gap-px"
>
{logs.map((log, index) => {
if (index === logs.length - 1) {
return (
<div ref={firstObserver} key={index}>
<Log log={log} />
</div>
);
} else return <Log key={index} log={log} />;
})}
</div>
</div>
);
}
function Log({ log }: { log: LogData }) {
const logLevelColor = {
info: 'text-green-500',
error: 'text-red-500',
warning: 'text-yellow-500',
};
const { id, action, timestamp, ...filteredLog } = log;
return (
<details className="group bg-transparent [&_summary::-webkit-details-marker]:hidden w-full hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal">
<summary className="flex flex-row items-center gap-2 text-gray-900 cursor-pointer p-2 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
<img
src={ChevronRight}
alt="chevron-right"
className="w-3 h-3 transition duration-300 group-open:rotate-90"
/>
<span className="flex flex-row gap-2">
<h2 className="text-xs text-black/60 dark:text-bright-gray">{`${log.timestamp}`}</h2>
<h2 className="text-xs text-[#913400] dark:text-[#DF5200]">{`[${log.action}]`}</h2>
<h2
className={`text-xs ${logLevelColor[log.level]}`}
>{`${log.question}`}</h2>
</span>
</summary>
<div className="px-4 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
<p className="px-2 leading-relaxed text-gray-700 dark:text-gray-400 text-xs">
{JSON.stringify(filteredLog, null, 2)}
</p>
<div className="my-px w-8">
<CoppyButton
text={JSON.stringify(filteredLog)}
colorLight="transparent"
/>
</div>
</div>
</details>
);
}

View File

@@ -11,9 +11,11 @@ import {
selectSourceDocs,
setSourceDocs,
} from '../preferences/preferenceSlice';
import Analytics from './Analytics';
import APIKeys from './APIKeys';
import Documents from './Documents';
import General from './General';
import Logs from './Logs';
import Widgets from './Widgets';
export default function Settings() {
@@ -23,6 +25,8 @@ export default function Settings() {
t('settings.general.label'),
t('settings.documents.label'),
t('settings.apiKeys.label'),
t('settings.analytics.label'),
t('settings.logs.label'),
];
const [activeTab, setActiveTab] = React.useState(t('settings.general.label'));
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(
@@ -128,6 +132,10 @@ export default function Settings() {
);
case t('settings.apiKeys.label'):
return <APIKeys />;
case t('settings.analytics.label'):
return <Analytics />;
case t('settings.logs.label'):
return <Logs />;
default:
return null;
}

View File

@@ -0,0 +1,20 @@
export type APIKeyData = {
id: string;
name: string;
key: string;
source: string;
prompt_id: string;
chunks: string;
};
export type LogData = {
id: string;
action: string;
level: 'info' | 'error' | 'warning';
user: string;
question: string;
response: string;
sources: Record<string, any>[];
retriever_params: Record<string, any>;
timestamp: string;
};

View File

@@ -275,7 +275,7 @@ function Upload({
} else
setRedditData({
...redditData,
[name]: value,
[name]: name === 'number_posts' ? parseInt(value) : value,
});
};
@@ -321,6 +321,7 @@ function Upload({
colorVariant="gray"
value={docName}
onChange={(e) => setDocName(e.target.value)}
borderVariant="thin"
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
@@ -356,6 +357,7 @@ function Upload({
{activeTab === 'remote' && (
<>
<Dropdown
border="border"
options={urlOptions}
selectedValue={urlType}
onSelect={(value: { label: string; value: string }) =>
@@ -371,6 +373,7 @@ function Upload({
type="text"
value={urlName}
onChange={(e) => setUrlName(e.target.value)}
borderVariant="thin"
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
@@ -382,6 +385,7 @@ function Upload({
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
borderVariant="thin"
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
@@ -390,68 +394,83 @@ function Upload({
</div>
</>
) : (
<>
<Input
placeholder="Enter client ID"
type="text"
name="client_id"
value={redditData.client_id}
onChange={handleChange}
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.id')}
</span>
<div className="flex flex-col gap-1 mt-2">
<div>
<Input
placeholder="Enter client ID"
type="text"
name="client_id"
value={redditData.client_id}
onChange={handleChange}
borderVariant="thin"
></Input>
<div className="relative bottom-[52px] left-2">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.id')}
</span>
</div>
</div>
<Input
placeholder="Enter client secret"
type="text"
name="client_secret"
value={redditData.client_secret}
onChange={handleChange}
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.secret')}
</span>
<div>
<Input
placeholder="Enter client secret"
type="text"
name="client_secret"
value={redditData.client_secret}
onChange={handleChange}
borderVariant="thin"
></Input>
<div className="relative bottom-[52px] left-2">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.secret')}
</span>
</div>
</div>
<Input
placeholder="Enter user agent"
type="text"
name="user_agent"
value={redditData.user_agent}
onChange={handleChange}
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.agent')}
</span>
<div>
<Input
placeholder="Enter user agent"
type="text"
name="user_agent"
value={redditData.user_agent}
onChange={handleChange}
borderVariant="thin"
></Input>
<div className="relative bottom-[52px] left-2">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.agent')}
</span>
</div>
</div>
<Input
placeholder="Enter search queries"
type="text"
name="search_queries"
value={redditData.search_queries}
onChange={handleChange}
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.searchQueries')}
</span>
<div>
<Input
placeholder="Enter search queries"
type="text"
name="search_queries"
value={redditData.search_queries}
onChange={handleChange}
borderVariant="thin"
></Input>
<div className="relative bottom-[52px] left-2">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.searchQueries')}
</span>
</div>
</div>
<Input
placeholder="Enter number of posts"
type="number"
name="number_posts"
value={redditData.number_posts}
onChange={handleChange}
></Input>
<div className="relative bottom-12 left-2 mt-[-20px]">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.numberOfPosts')}
</span>
<div>
<Input
placeholder="Enter number of posts"
type="number"
name="number_posts"
value={redditData.number_posts}
onChange={handleChange}
borderVariant="thin"
></Input>
<div className="relative bottom-[52px] left-2">
<span className="bg-white px-2 text-xs text-gray-4000 dark:bg-outer-space dark:text-silver">
{t('modals.uploadDoc.reddit.numberOfPosts')}
</span>
</div>
</div>
</>
</div>
)}
</>
)}

View File

@@ -0,0 +1,77 @@
import { Chart as ChartJS } from 'chart.js';
const getOrCreateLegendList = (
chart: ChartJS,
id: string,
): HTMLUListElement => {
const legendContainer = document.getElementById(id);
let listContainer = legendContainer?.querySelector('ul') as HTMLUListElement;
if (!listContainer) {
listContainer = document.createElement('ul');
listContainer.style.display = 'flex';
listContainer.style.flexDirection = 'row';
listContainer.style.margin = '0';
listContainer.style.padding = '0';
legendContainer?.appendChild(listContainer);
}
return listContainer;
};
export const htmlLegendPlugin = {
id: 'htmlLegend',
afterUpdate(chart: ChartJS, args: any, options: { containerID: string }) {
const ul = getOrCreateLegendList(chart, options.containerID);
while (ul.firstChild) {
ul.firstChild.remove();
}
const items =
chart.options.plugins?.legend?.labels?.generateLabels?.(chart) || [];
items.forEach((item: any) => {
const li = document.createElement('li');
li.style.alignItems = 'center';
li.style.cursor = 'pointer';
li.style.display = 'flex';
li.style.flexDirection = 'row';
li.style.marginLeft = '10px';
li.onclick = () => {
chart.setDatasetVisibility(
item.datasetIndex,
!chart.isDatasetVisible(item.datasetIndex),
);
chart.update();
};
const boxSpan = document.createElement('span');
boxSpan.style.background = item.fillStyle;
boxSpan.style.borderColor = item.strokeStyle;
boxSpan.style.borderWidth = item.lineWidth + 'px';
boxSpan.style.display = 'inline-block';
boxSpan.style.flexShrink = '0';
boxSpan.style.height = '10px';
boxSpan.style.marginRight = '10px';
boxSpan.style.width = '10px';
boxSpan.style.borderRadius = '10px';
const textContainer = document.createElement('p');
textContainer.style.fontSize = '12px';
textContainer.style.color = item.fontColor;
textContainer.style.margin = '0';
textContainer.style.padding = '0';
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
const text = document.createTextNode(item.text);
textContainer.appendChild(text);
li.appendChild(boxSpan);
li.appendChild(textContainer);
ul.appendChild(li);
});
},
};

View File

@@ -0,0 +1,20 @@
export function formatDate(dateString: string): string {
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateString)) {
const dateTime = new Date(dateString);
return dateTime.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
} else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(dateString)) {
const dateTime = new Date(dateString);
return dateTime.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
} else if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
} else {
return dateString;
}
}