import React, { useMemo } from "react"; import { useEffect, useState } from "react"; import { create } from "zustand"; import { useDashboard } from "../../layouts/Dashboard/useDashboard"; import { useBackend } from "../../layouts/Wrapper"; import { twMerge } from "tailwind-merge"; import { useMaxHeight } from "../../lib/useMaxHeight"; import { Title, Text } from "../../tremor/Text"; import { DataSource, WidgetSuggestion } from "@onvo-ai/js"; import { Badge } from "../../tremor/Badge"; import { Button } from "../../tremor/Button"; import { PlusIcon, TrashIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { Popover, PopoverContent, PopoverTrigger } from "../../tremor/Popover"; import { Tooltip } from "../../tremor/Tooltip"; import { Icon } from "../../tremor/Icon"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../tremor/Tabs"; import { WidgetLibrary } from "../WidgetLibrary"; import { Card } from "../../tremor/Card"; export const useWidgetSuggestionsModal = create<{ open: boolean; setOpen: ( open: boolean, ) => void; suggestions: WidgetSuggestion[]; setSuggestions: ( suggestions: WidgetSuggestion[], ) => void; }>((set) => ({ open: false, setOpen: ( op: boolean, ) => set({ open: op }), suggestions: [], setSuggestions: ( suggestions: WidgetSuggestion[], ) => set({ suggestions: suggestions }), })); const generateDatasourceHash = async (datasources: DataSource[]) => { const ids = datasources.map(a => a.id).sort().join(","); const encoder = new TextEncoder(); const data = encoder.encode(ids); // Compute SHA-256 hash const hashBuffer = await crypto.subtle.digest("SHA-256", data); // Convert ArrayBuffer -> hex string const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); // Truncate to desired length return hashHex.slice(0, 16); } export const WidgetSuggestionsModal: React.FC<{}> = ({ }) => { const { dashboard, refreshWidgets, tab, datasources } = useDashboard(); const { backend, adminMode, containerRef } = useBackend(); const { open, setOpen, suggestions, setSuggestions } = useWidgetSuggestionsModal(); const { lg, sm } = useMaxHeight(); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(false); const [hash, setHash] = useState(""); const [adding, setAdding] = useState<{ [id: string]: boolean }>({}); const [deleting, setDeleting] = useState<{ [id: string]: boolean }>({}); useEffect(() => { if (!dashboard || !backend) return; generateDatasourceHash(datasources).then((h) => setHash(h)); }, [dashboard, datasources]); const createWidget = async (suggestion: WidgetSuggestion) => { if (!dashboard || !backend) return; setAdding({ ...adding, [suggestion.id]: true }); let widget = await backend.widgets.create({ dashboard: dashboard.id, layouts: { lg: { x: 0, y: lg, w: 4, h: suggestion.type === "metric" ? 8 : 20, }, sm: { x: 0, y: sm, w: 3, h: suggestion.type === "metric" ? 8 : 20, }, }, use_in_library: false, use_in_chat: false, title: suggestion.title || "", team: dashboard.team, code: "", messages: [], settings: {}, error: null, tab: tab || 0, config: {}, engine: "python-v1", type: suggestion.type || "metric", drilldown_widget: null, }); refreshWidgets(backend); await backend.dashboard(dashboard.id).widgetSuggestions.delete(suggestion.id); setSuggestions(suggestions.filter((s) => s.id !== suggestion.id)); await backend.widget(widget.id).updatePrompts([{ role: "user", content: suggestion.description || "", }]); refreshWidgets(backend); setAdding({ ...adding, [suggestion.id]: false }); } const getWidgetSuggestions = async () => { if (!dashboard || !backend || !adminMode) return; setLoading(true); const suggestions = await backend.dashboard(dashboard.id).widgetSuggestions.list(); setLoading(false); setSuggestions(suggestions); } const generateWidgetSuggestions = async (clearExisting?: boolean) => { if (!dashboard || !backend) return; setGenerating(true); if (clearExisting) { await Promise.all(suggestions.filter((s) => s.datasource_hash !== hash).map((s) => backend.dashboard(dashboard.id).widgetSuggestions.delete(s.id))); } await backend.dashboard(dashboard.id).generateWidgetSuggestions(); setGenerating(false); getWidgetSuggestions(); } const clearWidgetSuggestion = async (id: string) => { if (!dashboard || !backend) return; setDeleting({ ...deleting, [id]: true }); await backend.dashboard(dashboard.id).widgetSuggestions.delete(id); getWidgetSuggestions(); setDeleting({ ...deleting, [id]: false }); } useEffect(() => { getWidgetSuggestions(); }, [dashboard, adminMode, backend]); const hashMismatch = useMemo(() => { let mismatch = false; suggestions.forEach((s) => { if (s.datasource_hash !== hash) { mismatch = true; } }); return mismatch; }, [hash, suggestions]); const AddToDashboardEnabled = useMemo(() => { if (adminMode) return true; if (dashboard?.settings?.can_create_widgets) return true; return false; }, [dashboard, adminMode]); if (!adminMode) return <>; return (
Suggestions Library
setOpen(false)} />
{!loading && hashMismatch && (
Your datasources have changed since the last time you generated widget suggestions. Please generate new suggestions.
)} {suggestions.filter((suggestion) => suggestion.datasource_hash === hash).map((suggestion) => (
{suggestion.title} {suggestion.type}
{suggestion.description}
{adminMode && ( )}
{AddToDashboardEnabled && ( )}
))} {!loading && !hashMismatch && (
{suggestions.length >= 30 ? ( ) : ( )}
)}
setOpen(false)} />
); };