import { Label, Text } from "../../tremor/Text"; import { Icon } from "../../tremor/Icon"; import { memo, useEffect, useMemo, useRef, useState } from "react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { toast, Toaster } from "sonner"; import { QuestionMessage } from "../../components/QuestionMessage"; import { QuestionHistory } from "../../components/QuestionHistory"; import React from "react"; import { useBackend } from "../Wrapper"; import { useDashboard } from "../Dashboard/useDashboard"; import { ChartLoader } from "../../components/ChartLoader"; import { LogType, Message, Question, Widget } from "@onvo-ai/js"; import { ChevronRightIcon } from "@heroicons/react/16/solid"; import { useTheme } from "../Dashboard/useTheme"; import { PromptInput } from "../../components/PromptInput"; import { useMaxHeight } from "../../lib/useMaxHeight"; import { ChevronDoubleRightIcon, ChevronDownIcon, ClockIcon, PencilSquareIcon, PlusIcon, ViewfinderCircleIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { create } from "zustand"; import { Tooltip } from "../../tremor/Tooltip"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "../../tremor/DropdownMenu"; import { useIsMobile } from "../../lib/utils"; dayjs.extend(relativeTime); export const useCopilot = create<{ open: boolean; variant?: "default" | "modal"; setOpen: ( open: boolean, ) => void; setVariant: ( variant: "default" | "modal", ) => void; }>((set) => ({ open: false, variant: "default" as const, setVariant: ( variant: "default" | "modal", ) => { localStorage.setItem("onvo-copilot-variant", variant); set({ variant }); }, setOpen: ( op: boolean, ) => { let variant = localStorage.getItem("onvo-copilot-variant"); if (variant) { set({ open: op, variant: variant as "default" | "modal" }) } else { set({ open: op, variant: "default" }) } } })); const CopilotRaw: React.FC<{ coupled?: boolean; }> = ({ coupled }): React.ReactNode => { const { backend, setContainerRef, } = useBackend(); const { dashboard, refreshWidgets, tab: dashboardTab } = useDashboard(); const theme = useTheme(); const { open, setOpen, setVariant, variant } = useCopilot(); const scroller = useRef(null); const localRef = useRef(null); const { lg, sm } = useMaxHeight(); const isMobile = useIsMobile(); const [questionLoading, setQuestionLoading] = useState(false); const [selectedQuestion, setSelectedQuestion] = useState(); const [messages, setMessages] = useState([]); const [userQuery, setUserQuery] = useState(""); useEffect(() => { if (isMobile) { setVariant("modal"); } }, [isMobile]); useEffect(() => { if (localRef?.current && !coupled) { setContainerRef(localRef); } }, [localRef, coupled]) useEffect(() => { if (!(window as any).Onvo) { (window as any).Onvo = {}; } (window as any).Onvo.setCopilotOpen = (val: boolean) => setOpen(val); }, []); useEffect(() => { if (!open) { setMessages([]); setSelectedQuestion(undefined); setUserQuery(""); } }, [open]); useEffect(() => { if (selectedQuestion) { getMessages(selectedQuestion.id); } else { setMessages([]); } }, [selectedQuestion, userQuery]); const getMessages = async (id: string) => { let msg = await backend?.question(id).getMessages(); if (!msg || msg.length === 0) { if (userQuery && userQuery.trim() !== "") { msg = [{ role: "user", content: userQuery, created_at: dayjs().toISOString(), id: "new", } as any]; } } setMessages(msg && msg.length > 0 ? msg : []); setTimeout(scrollToBottom, 100); } const scrollToBottom = () => { if (scroller.current) { scroller.current.scrollTop = scroller.current.scrollHeight; } }; const addToDashboard = async (widget: Widget, library?: boolean) => { if (!backend) return; toast.promise( async () => { let wid = await backend.widgets.create({ ...widget, use_in_library: library || false, use_in_chat: false, tab: dashboardTab || 0, layouts: { lg: { x: 0, y: lg, w: 4, h: widget.type === "metric" ? 8 : 20, }, sm: { x: 0, y: sm, w: 3, h: widget.type === "metric" ? 8 : 20, }, } }); await backend.widget(wid.id).updateCache(); return wid; }, { loading: library ? "Adding widget to library..." : "Adding widget to dashboard...", success: (widget) => { refreshWidgets(backend); setSelectedQuestion(undefined); setMessages([]); setOpen(false); setUserQuery(""); if (backend) { backend.logs.create({ type: LogType.EditWidget, dashboard: widget.dashboard, widget: widget.id, }) } if (library) { return "Widget added to library"; } else { return "Widget added to dashboard"; } }, error: (error) => library ? "Error adding widget to library: " + error.message : "Error adding widget to dashboard: " + error.message, } ); }; const createQuestion = async (query: string) => { if (!dashboard) return; try { let response = await backend?.questions.create({ dashboard: dashboard?.id, query: query, }); if (!response) { throw new Error("Failed to create question."); } setUserQuery(query); setSelectedQuestion(response); return response.id; } catch (e: any) { toast.error("Failed to create question: ", e.message); } }; const handleAskQuestion = async (query: string) => { let qId = selectedQuestion?.id; if (!qId) { qId = await createQuestion(query); } if (!qId) return; try { setQuestionLoading(true); await backend?.question(qId).completion(query, log => { getMessages(qId); setUserQuery(""); }); getMessages(qId); setQuestionLoading(false); } catch (error: any) { console.error("Error asking question:", error); toast.error(error.message, { duration: Infinity, closeButton: true }); setQuestionLoading(false); } }; const handleEditMessage = async (messageId: string, content: string) => { if (!selectedQuestion) return; try { setQuestionLoading(true); const editedMessage = messages.find((msg) => msg.id === messageId); setMessages((prev) => { return prev.filter((msg) => { if (msg.created_at && editedMessage?.created_at && msg.created_at > editedMessage.created_at) { return false } return true; }); }); // waitForJob let response = await backend?.question(selectedQuestion.id).updateMessage(messageId, content); if (!response) { toast.error("Failed to edit message."); setQuestionLoading(false); return; } getMessages(selectedQuestion.id); setQuestionLoading(false); } catch (error) { console.error("Error asking question:", error); setQuestionLoading(false); } }; const questionMessageList = useMemo(() => { return messages.map((message, index) => ( { handleAskQuestion(msg); }} onEdit={(msg) => { handleEditMessage(message.id, msg); }} onAdd={(widget, library) => { addToDashboard(widget, library); }} /> )); }, [messages, questionLoading]); return ( <>
{!coupled && }
{(dashboard?.settings?.copilot_description || "").trim() !== "" && ( {dashboard?.settings?.copilot_description || ""} )}
{ setSelectedQuestion(undefined); setMessages([]); }} /> { setSelectedQuestion(q); setMessages([]); }} variant="dropdown" /> {coupled && !isMobile && ( { setVariant(variant === "default" ? "modal" : "default"); }} /> )} { setOpen(false); }} />
{!selectedQuestion ? ( <>
{ setSelectedQuestion(q); setMessages([]); }} limit={4} variant="list" />
) : (
{questionMessageList} {questionLoading && ( )}
)} { handleAskQuestion(val); }} />
); }; export const Copilot: React.FC<{ coupled?: boolean; }> = ({ coupled }): React.ReactNode => { const { variant, open, setVariant } = useCopilot(); useEffect(() => { let storedVariant = localStorage.getItem("onvo-copilot-variant"); if (!coupled) { setVariant("modal"); return; } if (storedVariant) { setVariant(storedVariant as "default" | "modal"); } else { setVariant("default"); } }, []); if (variant === "default") { return ( ) } return (<> ) }