import { createEffect, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js" import { animate, type AnimationPlaybackControls } from "motion" import { useI18n } from "../context/i18n" import { createStore } from "solid-js/store" import { Collapsible } from "./collapsible" import type { IconProps } from "./icon" import { TextShimmer } from "./text-shimmer" export type TriggerTitle = { title: string titleClass?: string subtitle?: string subtitleClass?: string args?: string[] argsClass?: string action?: JSX.Element } const isTriggerTitle = (val: any): val is TriggerTitle => { return ( typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node)) ) } export interface BasicToolProps { icon: IconProps["name"] trigger: TriggerTitle | JSX.Element children?: JSX.Element status?: string hideDetails?: boolean defaultOpen?: boolean forceOpen?: boolean defer?: boolean locked?: boolean animated?: boolean onSubtitleClick?: () => void } const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 } export function BasicTool(props: BasicToolProps) { const [state, setState] = createStore({ open: props.defaultOpen ?? false, ready: props.defaultOpen ?? false, }) const open = () => state.open const ready = () => state.ready const pending = () => props.status === "pending" || props.status === "running" let frame: number | undefined const cancel = () => { if (frame === undefined) return cancelAnimationFrame(frame) frame = undefined } onCleanup(cancel) createEffect(() => { if (props.forceOpen) setState("open", true) }) createEffect( on( open, (value) => { if (!props.defer) return if (!value) { cancel() setState("ready", false) return } cancel() frame = requestAnimationFrame(() => { frame = undefined if (!open()) return setState("ready", true) }) }, { defer: true }, ), ) // Animated height for collapsible open/close let contentRef: HTMLDivElement | undefined let heightAnim: AnimationPlaybackControls | undefined const initialOpen = open() createEffect( on( open, (isOpen) => { if (!props.animated || !contentRef) return heightAnim?.stop() if (isOpen) { contentRef.style.overflow = "hidden" heightAnim = animate(contentRef, { height: "auto" }, SPRING) heightAnim.finished.then(() => { if (!contentRef || !open()) return contentRef.style.overflow = "visible" contentRef.style.height = "auto" }) } else { contentRef.style.overflow = "hidden" heightAnim = animate(contentRef, { height: "0px" }, SPRING) } }, { defer: true }, ), ) onCleanup(() => { heightAnim?.stop() }) const handleOpenChange = (value: boolean) => { if (pending()) return if (props.locked && !value) return setState("open", value) } return ( {(trigger) => ( { if (props.onSubtitleClick) { e.stopPropagation() props.onSubtitleClick() } }} > {trigger().subtitle} {(arg) => ( {arg} )} {trigger().action} )} {props.trigger as JSX.Element} {props.children} {props.children} ) } function label(input: Record | undefined) { const keys = ["description", "query", "url", "filePath", "path", "pattern", "name"] return keys.map((key) => input?.[key]).find((value): value is string => typeof value === "string" && value.length > 0) } function args(input: Record | undefined) { if (!input) return [] const skip = new Set(["description", "query", "url", "filePath", "path", "pattern", "name"]) return Object.entries(input) .filter(([key]) => !skip.has(key)) .flatMap(([key, value]) => { if (typeof value === "string") return [`${key}=${value}`] if (typeof value === "number") return [`${key}=${value}`] if (typeof value === "boolean") return [`${key}=${value}`] return [] }) .slice(0, 3) } export function GenericTool(props: { tool: string status?: string hideDetails?: boolean input?: Record }) { const i18n = useI18n() return ( ) }