From 733540066768b3b64deb98a4dc2d83e18eeef4ec Mon Sep 17 00:00:00 2001 From: ah7255703 Date: Sat, 27 Jan 2024 18:52:49 +0200 Subject: [PATCH 1/7] Add analytics data retrieval functions to dashboard This commit adds two new functions to retrieve analytics data from the backend API. The functions `getMostCalledActions` and `getAnalyticsData` are implemented in the `analytics.ts` file. The `getMostCalledActions` function retrieves the most called actions for a specific bot, while the `getAnalyticsData` function retrieves various counts related to analytics. --- dashboard/data/analytics.ts | 20 ++++++++++++++++++++ dashboard/data/copilot.ts | 8 -------- 2 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 dashboard/data/analytics.ts diff --git a/dashboard/data/analytics.ts b/dashboard/data/analytics.ts new file mode 100644 index 000000000..6f2b824cf --- /dev/null +++ b/dashboard/data/analytics.ts @@ -0,0 +1,20 @@ +import axios from "axios"; +import { baseUrl } from "./base-url"; + +const instance = axios.create({ + baseURL: baseUrl + "/backend", +}); + +// /chat/actions/most_called/:bot_id +export async function getMostCalledActions(bot_id: string) { + return instance.get(`/chat/actions/most_called/${bot_id}`); +} + +// analytics data +export async function getAnalyticsData(id: string) { + return instance.get<{ + api_called_count: number; + knowledgebase_called_count:number; + other_count: number; + }[]>(`analytics/${id}`) + } \ No newline at end of file diff --git a/dashboard/data/copilot.ts b/dashboard/data/copilot.ts index 732cbc6f9..75498146d 100644 --- a/dashboard/data/copilot.ts +++ b/dashboard/data/copilot.ts @@ -77,11 +77,3 @@ export async function deleteVariableByKey(id: string, name: string) { return await instance.delete<{ message: string }>(`/${id}/variable/${name}`) } -// analytics data -export async function getAnalyticsData(id: string) { - return (await instance.get<{ - api_called_count: number; - knowledgebase_called_count:number; - other_count: number; - }[]>(`/${id}/analytics`)).data -} \ No newline at end of file From 50cadad845c394acca81d8b135f50d1bde76d4e0 Mon Sep 17 00:00:00 2001 From: ah7255703 Date: Sat, 27 Jan 2024 18:53:14 +0200 Subject: [PATCH 2/7] Add CopilotCard component and refactor CopilotsContainer --- dashboard/app/(main)/_parts/CopilotCard.tsx | 59 +++++++++++++++++++ .../app/(main)/_parts/CopilotsContainer.tsx | 47 +++------------ 2 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 dashboard/app/(main)/_parts/CopilotCard.tsx diff --git a/dashboard/app/(main)/_parts/CopilotCard.tsx b/dashboard/app/(main)/_parts/CopilotCard.tsx new file mode 100644 index 000000000..ff6a85174 --- /dev/null +++ b/dashboard/app/(main)/_parts/CopilotCard.tsx @@ -0,0 +1,59 @@ +import { Stack } from "@/components/ui/Stack"; +import { CopilotType } from "@/data/copilot"; +import { Link } from "@/lib/router-events"; +import { motion } from "framer-motion"; +import { GalleryHorizontalEnd } from "lucide-react"; +import { format } from "timeago.js"; + +const IsoMorphicAnimatedLink = (animated:boolean) => animated ? motion(Link) : Link; + +export function CopilotCard( + { + copilot, + index, + animated = true + }:{ + copilot: CopilotType, + index: number + animated?: boolean + } +) { + const copilotUrl = `/copilot/${copilot.id}`; + const AnimatedLink = IsoMorphicAnimatedLink(animated); + return ( + +
+
+ +
+
+ +

+ {copilot.name} +

+

+ Created{" "} + {format(copilot.created_at)} +

+
+
+ ) +} diff --git a/dashboard/app/(main)/_parts/CopilotsContainer.tsx b/dashboard/app/(main)/_parts/CopilotsContainer.tsx index 0ca6e27af..b98564fa6 100644 --- a/dashboard/app/(main)/_parts/CopilotsContainer.tsx +++ b/dashboard/app/(main)/_parts/CopilotsContainer.tsx @@ -1,6 +1,5 @@ "use client"; import React from "react"; -import { GalleryHorizontalEnd } from "lucide-react"; import { Link } from "@/lib/router-events"; import useSwr from "swr"; import { CopilotType, listCopilots } from "@/data/copilot"; @@ -10,15 +9,14 @@ import { EmptyBlock } from "@/components/domain/EmptyBlock"; import { filterAtom } from "./Search"; import { useAtomValue } from "jotai"; import { Button } from "@/components/ui/button"; -import { format } from "timeago.js"; -import { motion, AnimatePresence } from 'framer-motion'; -import { Stack } from "@/components/ui/Stack"; +import { AnimatePresence } from "framer-motion"; +import { CopilotCard } from "./CopilotCard"; + function orderByCreatedAt(copilots: CopilotType[]) { return copilots.sort((a, b) => { return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); }); } -const AnimatedLink = motion(Link); export function CopilotsContainer() { const { data: copilots, isLoading } = useSwr("copilotsList", listCopilots); @@ -67,44 +65,13 @@ export function CopilotsContainer() { ) : (
{orderByCreatedAt($copilots).map((copilot, index) => { - const copilotUrl = "/copilot/" + copilot.id; return ( - -
-
- -
-
- -

- {copilot.name} -

-

- Created{" "} - {format(copilot.created_at)} -

-
-
+
); })} From 579bbf8b91f7553ba289712dfe054aa9876397e3 Mon Sep 17 00:00:00 2001 From: ah7255703 Date: Sat, 27 Jan 2024 23:22:50 +0200 Subject: [PATCH 3/7] Update global styles in dashboard app --- dashboard/app/globals.css | 47 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/dashboard/app/globals.css b/dashboard/app/globals.css index 2de38e74d..2c33c46fb 100644 --- a/dashboard/app/globals.css +++ b/dashboard/app/globals.css @@ -76,9 +76,11 @@ * { @apply border-border; } + body { @apply bg-background text-foreground; } + .primary-shadow { box-shadow: rgba(19, 33, 68, 0.08) 0px -1px 0px 1px inset, @@ -94,19 +96,25 @@ } .no-scrollbar { - -ms-overflow-style: none; /* Internet Explorer 10+ */ - scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; + /* Internet Explorer 10+ */ + scrollbar-width: none; + /* Firefox */ } + .no-scrollbar::-webkit-scrollbar { - display: none; /* Safari and Chrome */ + display: none; + /* Safari and Chrome */ } } + .loading-el { position: relative; overflow: hidden; pointer-events: none; color: transparent; } + .loading-el::after { content: ""; position: absolute; @@ -125,43 +133,76 @@ border-width: 3px; @apply h-5 w-5 animate-spin rounded-full border-primary border-t-transparent; } + * { min-width: 0; min-height: 0; } + /* select labels beside required inputs */ .required-label::after { content: "*"; color: hsl(var(--destructive)); margin-left: 0.25rem; } + .reset-input { @apply m-0 h-10 rounded-md border border-border bg-background bg-white px-3 py-2 text-sm shadow-sm ring-offset-accent transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:outline-none focus-visible:border-primary focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[valid=false]:!border-destructive; } + .copilot__container:not(:hover) .copilot .box { @apply bg-secondary/70; } + .copilot__container:is(:hover) .copilot .box { @apply bg-secondary; } + .copilot__container:is(:hover) .copilot .box:hover { @apply bg-accent; } + /* adding animation (opacity) */ @keyframes blink-animation { 0% { opacity: 0; } + 50% { opacity: 1; } + 70% { opacity: 0; } + 100% { opacity: 1; } } + .blink { animation: blink-animation 1s steps(5, start) both; } + +.cross-dots-bg { + background: + radial-gradient(black 3px, transparent 4px), + radial-gradient(black 3px, transparent 4px), + linear-gradient(#fff 4px, transparent 0), + linear-gradient(45deg, transparent 74px, transparent 75px, #a4a4a4 75px, #a4a4a4 76px, transparent 77px, transparent 109px), + linear-gradient(-45deg, transparent 75px, transparent 76px, #a4a4a4 76px, #a4a4a4 77px, transparent 78px, transparent 109px), + #fff; + background-size: 109px 109px, 109px 109px, 100% 6px, 109px 109px, 109px 109px; + background-position: 54px 55px, 0px 0px, 0px 0px, 0px 0px, 0px 0px; +} + +.diagonal-stripes { + --stripe-width: 10px; + --spacing: calc(var(--stripe-width) * 5); + --stripe-color: rgba(0, 0, 0, 0.2); + --stripe-background: transparent; + background-image: linear-gradient(45deg, var(--stripe-color) 25%, transparent 25%, transparent 50%, var(--stripe-color) 50%, var(--stripe-color) 75%, transparent 75%, transparent); + background-size: var(--spacing) var(--spacing); + background-color: var(--stripe-background); +} \ No newline at end of file From 12a52bf00c330878ebe2b7bfcb52700fe7e9a053 Mon Sep 17 00:00:00 2001 From: ah7255703 Date: Sat, 27 Jan 2024 23:23:07 +0200 Subject: [PATCH 4/7] Add Counter component and update Skeleton component --- dashboard/components/ui/Counter.tsx | 37 ++++++++++++++++++++++++++++ dashboard/components/ui/skeleton.tsx | 7 +++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 dashboard/components/ui/Counter.tsx diff --git a/dashboard/components/ui/Counter.tsx b/dashboard/components/ui/Counter.tsx new file mode 100644 index 000000000..1d1571d50 --- /dev/null +++ b/dashboard/components/ui/Counter.tsx @@ -0,0 +1,37 @@ +'use client'; +import { useEffect, useRef, useState } from "react"; +import { useInView, useMotionValue, useSpring } from "framer-motion"; + + +export function Counter({ + value, + direction = "up", +}: { + value?: number; + direction?: "up" | "down"; +}) { + const ref = useRef(null); + const motionValue = useMotionValue(0) + const springValue = useSpring(motionValue); + const isInView = useInView(ref, { once: true, margin: "0px" }); + const [renderedValue, setRenderedValue] = useState(0); + useEffect(() => { + if (isInView) { + motionValue.set(direction === "down" ? 0 : value ?? 0); + + if (value === 0) { + motionValue.set(0); + } + } + }, [motionValue, isInView, value, direction]); + + useEffect( + () => + springValue.on("change", (latest) => { + setRenderedValue(Math.round(latest)); + }), + [springValue] + ); + + return {renderedValue}; +} \ No newline at end of file diff --git a/dashboard/components/ui/skeleton.tsx b/dashboard/components/ui/skeleton.tsx index a78a3c4e8..82b06ed64 100644 --- a/dashboard/components/ui/skeleton.tsx +++ b/dashboard/components/ui/skeleton.tsx @@ -3,8 +3,13 @@ import React from "react" function Skeleton({ className, + isLoading, + children, ...props -}: React.HTMLAttributes) { +}: React.HTMLAttributes & { isLoading?: boolean }) { + if (!isLoading) { + return <>{children} + } return (
Date: Sat, 27 Jan 2024 23:23:15 +0200 Subject: [PATCH 5/7] Refactor analytics.ts to use TypeScript and add type definitions --- dashboard/data/analytics.ts | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/dashboard/data/analytics.ts b/dashboard/data/analytics.ts index 6f2b824cf..1d95e9888 100644 --- a/dashboard/data/analytics.ts +++ b/dashboard/data/analytics.ts @@ -1,20 +1,36 @@ import axios from "axios"; import { baseUrl } from "./base-url"; +import _ from "lodash"; const instance = axios.create({ - baseURL: baseUrl + "/backend", + baseURL: baseUrl + "/backend", }); +type MostCalledActionResponseType = { + count: number; + operation_id: string; +} // /chat/actions/most_called/:bot_id export async function getMostCalledActions(bot_id: string) { - return instance.get(`/chat/actions/most_called/${bot_id}`); + return (await instance.get(`/chat/actions/most_called/${bot_id}`)).data } // analytics data export async function getAnalyticsData(id: string) { - return instance.get<{ - api_called_count: number; - knowledgebase_called_count:number; - other_count: number; - }[]>(`analytics/${id}`) - } \ No newline at end of file + const data = (await instance.get<{ + api_called_count: number; + knowledgebase_called_count: number; + other_count: number; + total: number; + }[]>(`/chat/analytics/${id}`)).data + if (_.isEmpty(data)) { + return { + api_called_count: 0, + knowledgebase_called_count: 0, + other_count: 0, + total: 0, + } + } else { + return data[0] + } +} \ No newline at end of file From 57406f6d1a4b13ce92344281d17ac1b30743408e Mon Sep 17 00:00:00 2001 From: ah7255703 Date: Sat, 27 Jan 2024 23:23:33 +0200 Subject: [PATCH 6/7] Add analytics components for copilot dashboard --- .../analytics/AnalyticsActions.tsx | 38 +++++++++++ .../[copilot_id]/analytics/AnalyticsCards.tsx | 43 ++++++++++++ .../copilot/[copilot_id]/analytics/page.tsx | 65 ++----------------- 3 files changed, 88 insertions(+), 58 deletions(-) create mode 100644 dashboard/app/(copilot)/copilot/[copilot_id]/analytics/AnalyticsActions.tsx create mode 100644 dashboard/app/(copilot)/copilot/[copilot_id]/analytics/AnalyticsCards.tsx diff --git a/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/AnalyticsActions.tsx b/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/AnalyticsActions.tsx new file mode 100644 index 000000000..b1ffc40a8 --- /dev/null +++ b/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/AnalyticsActions.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { SimpleCard } from "@/components/domain/simple-card"; +import { getMostCalledActions } from "@/data/analytics"; +import { EChart } from "@kbox-labs/react-echarts"; +import useSWR from "swr"; + +type Props = { + copilot_id: string +} + +export function AnalyticsActions({ copilot_id }: Props) { + const { + data: mostCalledActions, + } = useSWR([copilot_id, 'copilot-mostCalledActions'], () => getMostCalledActions(copilot_id)); + const noMostCalledActions = mostCalledActions?.length === 0 || !mostCalledActions; + return ( + + {noMostCalledActions ?
+

No actions called yet

+
: action.operation_id) ?? [], + }} + yAxis={{ + type: 'value', + }} + series={{ + data: mostCalledActions?.map((action) => (action.count)) ?? [], + type: 'bar', + }} + />} +
+ ) +} diff --git a/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/AnalyticsCards.tsx b/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/AnalyticsCards.tsx new file mode 100644 index 000000000..45591f97f --- /dev/null +++ b/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/AnalyticsCards.tsx @@ -0,0 +1,43 @@ +"use client"; +import { SimpleCard } from "@/components/domain/simple-card"; +import useSWR from "swr"; +import { getAnalyticsData } from "@/data/analytics"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Counter } from "@/components/ui/Counter"; + +export function SimpleAnalyticsCards({ copilot_id }: { + copilot_id: string +}) { + const { + data: analyticsData, + isLoading + } = useSWR([copilot_id, 'copilot-analytics'], () => getAnalyticsData(copilot_id)) + return ( +
+ + +

+ +

+
+
+ + + +

+ +

+
+
+ + + +

+ +

+
+
+ +
+ ); +} diff --git a/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/page.tsx b/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/page.tsx index fe9f39def..809f7c1dc 100644 --- a/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/page.tsx +++ b/dashboard/app/(copilot)/copilot/[copilot_id]/analytics/page.tsx @@ -2,14 +2,17 @@ import { HeaderShell } from '@/components/domain/HeaderShell' import { SimpleCard } from '@/components/domain/simple-card' import { Stack } from '@/components/ui/Stack' import React from 'react' -import { EChart } from '@kbox-labs/react-echarts' +import { SimpleAnalyticsCards } from './AnalyticsCards'; +import { AnalyticsActions } from './AnalyticsActions' type Props = { params: { copilot_id: string } } -export default async function AnalyticsPage(props: Props) { + +export default function AnalyticsPage(props: Props) { + return ( @@ -18,63 +21,9 @@ export default async function AnalyticsPage(props: Props) {
-
- - 20 - - - 20 - -
+
- - - - - - +
From df56977feda20a11643cf0c028574d47aefd91bc Mon Sep 17 00:00:00 2001 From: ah7255703 Date: Sat, 27 Jan 2024 23:23:42 +0200 Subject: [PATCH 7/7] Refactor SimpleCard component to accept a className prop --- dashboard/components/domain/simple-card.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dashboard/components/domain/simple-card.tsx b/dashboard/components/domain/simple-card.tsx index 6397f479c..42e0e1380 100644 --- a/dashboard/components/domain/simple-card.tsx +++ b/dashboard/components/domain/simple-card.tsx @@ -1,14 +1,16 @@ import React from "react"; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../ui/card"; +import { cn } from "@/lib/utils"; -export function SimpleCard({ children, title, description }: { +export function SimpleCard({ children, title, description, className }: { children: React.ReactNode title: React.ReactNode description?: React.ReactNode + className?: string }) { - return + return - + {title} {description &&