Feat(webapp) AI screen UI improvements (#3269)
### Lots of UI improvements to the Prompts pages: #### New side menu icons <img width="234" height="110" alt="CleanShot 2026-03-25 at 14 26 33" src="https://github.com/user-attachments/assets/8039ee8f-92a0-477d-ac91-458dfa43020b" /> #### Compact horizontal start finish times so scanning generations list is consistent <img width="618" height="174" alt="CleanShot 2026-03-25 at 14 24 03" src="https://github.com/user-attachments/assets/7eb69475-d539-4e34-b0b5-5cd5119c1aea" /> #### Tidied up the metrics view <img width="1607" height="925" alt="CleanShot 2026-03-25 at 14 23 52" src="https://github.com/user-attachments/assets/d4bf2761-8e09-4d91-bfb8-e10d1508042f" /> #### Copiable metadata <img width="274" height="160" alt="CleanShot 2026-03-25 at 14 23 44" src="https://github.com/user-attachments/assets/487c804d-c000-4cd0-9a19-30c2681712df" /> #### Cleaner versions list <img width="463" height="264" alt="CleanShot 2026-03-25 at 14 23 28" src="https://github.com/user-attachments/assets/a2dfede7-0e7d-4f9c-8b01-00ba13070f70" /> #### Overall consistency improvements, shortcut keys and UI behaviours improvements <img width="2279" height="1349" alt="CleanShot 2026-03-25 at 14 23 02" src="https://github.com/user-attachments/assets/0c29257b-15a0-443c-b872-a9f4a7f6af13" /> --------- Co-authored-by: Eric Allam <eallam@icloud.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
export function AIMetricsIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.5 2C5.8013 2 6.0688 2.1928 6.16408 2.47864L6.53204 3.58252C6.67139 4.00057 6.99943 4.32861 7.41748 4.46796L8.52136 4.83592C8.8072 4.9312 9 5.1987 9 5.5C9 5.8013 8.8072 6.0688 8.52136 6.16408L7.41748 6.53204C6.99943 6.67139 6.67139 6.99943 6.53204 7.41748L6.16408 8.52136C6.0688 8.8072 5.8013 9 5.5 9C5.1987 9 4.9312 8.8072 4.83592 8.52136L4.46796 7.41748C4.32861 6.99943 4.00057 6.67139 3.58252 6.53204L2.47864 6.16408C2.1928 6.0688 2 5.8013 2 5.5C2 5.1987 2.1928 4.9312 2.47864 4.83592L3.58252 4.46796C4.00057 4.32861 4.32861 4.00057 4.46796 3.58252L4.83592 2.47864C4.9312 2.1928 5.1987 2 5.5 2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M18.6 2.40002C18.1226 2.40002 17.6648 2.58967 17.3272 2.92723C16.9896 3.2648 16.8 3.72263 16.8 4.20002V19.8C16.8 20.2774 16.9896 20.7353 17.3272 21.0728C17.6648 21.4104 18.1226 21.6 18.6 21.6H19.8C20.2774 21.6 20.7352 21.4104 21.0728 21.0728C21.4104 20.7353 21.6 20.2774 21.6 19.8V4.20002C21.6 3.72263 21.4104 3.2648 21.0728 2.92723C20.7352 2.58967 20.2774 2.40002 19.8 2.40002H18.6ZM11.4 7.20002C10.9226 7.20002 10.4648 7.38967 10.1272 7.72723C9.78964 8.0648 9.6 8.52263 9.6 9.00002V19.8C9.6 20.2774 9.78964 20.7353 10.1272 21.0728C10.4648 21.4104 10.9226 21.6 11.4 21.6H12.6C13.0774 21.6 13.5352 21.4104 13.8728 21.0728C14.2104 20.7353 14.4 20.2774 14.4 19.8V9.00002C14.4 8.52263 14.2104 8.0648 13.8728 7.72723C13.5352 7.38967 13.0774 7.20002 12.6 7.20002H11.4ZM4.2 12C3.72261 12 3.26477 12.1897 2.92721 12.5272C2.58964 12.8648 2.4 13.3226 2.4 13.8V19.8C2.4 20.2774 2.58964 20.7353 2.92721 21.0728C3.26477 21.4104 3.72261 21.6 4.2 21.6H5.4C5.87739 21.6 6.33523 21.4104 6.67279 21.0728C7.01036 20.7353 7.2 20.2774 7.2 19.8V13.8C7.2 13.3226 7.01036 12.8648 6.67279 12.5272C6.33523 12.1897 5.87739 12 5.4 12H4.2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function AIPromptsIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 3C14.5154 3 16.9845 3.20109 19.3916 3.58887C21.0075 3.8492 22.125 5.2653 22.125 6.85449V12.6455C22.125 14.2347 21.0075 15.6508 19.3916 15.9111C17.4843 16.2184 15.5379 16.4079 13.5605 16.4736C13.3313 16.4813 13.1196 16.5737 12.9668 16.7266L8.94043 20.7529C8.69912 20.9942 8.33578 21.0661 8.02051 20.9355C7.70544 20.8049 7.50001 20.4974 7.5 20.1562V16.2832C6.52707 16.189 5.56281 16.0649 4.6084 15.9111C2.99251 15.6508 1.875 14.2347 1.875 12.6455V6.85449C1.875 5.2653 2.99251 3.8492 4.6084 3.58887C7.01552 3.20109 9.48459 3 12 3ZM12 5.25C11.6127 5.25 11.2691 5.49785 11.1465 5.86523L10.6729 7.28418C10.4937 7.8215 10.0724 8.24359 9.53516 8.42285L8.11523 8.89648C7.74786 9.01906 7.5 9.3627 7.5 9.75C7.50001 10.1373 7.74786 10.4809 8.11523 10.6035L9.53516 11.0771C10.0722 11.2564 10.4937 11.6778 10.6729 12.2148L11.1465 13.6348C11.2691 14.0021 11.6127 14.25 12 14.25C12.3873 14.25 12.7309 14.0021 12.8535 13.6348L13.3271 12.2148C13.5063 11.6778 13.9278 11.2564 14.4648 11.0771L15.8848 10.6035C16.2521 10.4809 16.5 10.1373 16.5 9.75C16.5 9.3627 16.2521 9.01906 15.8848 8.89648L14.4648 8.42285C13.9276 8.24359 13.5063 7.8215 13.3271 7.28418L12.8535 5.86523C12.7309 5.49785 12.3873 5.25 12 5.25Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -4,16 +4,14 @@ import {
|
||||
BookOpenIcon,
|
||||
ChatBubbleLeftRightIcon,
|
||||
ClockIcon,
|
||||
DocumentTextIcon,
|
||||
PlusIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
RectangleGroupIcon,
|
||||
RectangleStackIcon,
|
||||
ServerStackIcon,
|
||||
SparklesIcon,
|
||||
Squares2X2Icon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useLocation } from "react-use";
|
||||
import { AIPromptsIcon } from "~/assets/icons/AIPromptsIcon";
|
||||
import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons";
|
||||
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
|
||||
import openBulkActionsPanel from "~/assets/images/open-bulk-actions-panel.png";
|
||||
@@ -25,21 +23,28 @@ import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { type MinimumEnvironment } from "~/presenters/SelectBestEnvironmentPresenter.server";
|
||||
import { NewBranchPanel } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route";
|
||||
import { GitHubSettingsPanel } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github";
|
||||
import {
|
||||
docsPath,
|
||||
v3BillingPath,
|
||||
v3CreateBulkActionPath,
|
||||
v3EnvironmentPath,
|
||||
v3EnvironmentVariablesPath,
|
||||
v3NewProjectAlertPath,
|
||||
v3NewSchedulePath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { AskAI } from "./AskAI";
|
||||
import { CodeBlock } from "./code/CodeBlock";
|
||||
import { InlineCode } from "./code/InlineCode";
|
||||
import { environmentFullTitle, EnvironmentIcon } from "./environments/EnvironmentLabel";
|
||||
import { Feedback } from "./Feedback";
|
||||
import { EnvironmentSelector } from "./navigation/EnvironmentSelector";
|
||||
import { Button, LinkButton } from "./primitives/Buttons";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsContent,
|
||||
ClientTabsList,
|
||||
ClientTabsTrigger,
|
||||
} from "./primitives/ClientTabs";
|
||||
import { Header1 } from "./primitives/Headers";
|
||||
import { InfoPanel } from "./primitives/InfoPanel";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
@@ -54,13 +59,6 @@ import {
|
||||
} from "./SetupCommands";
|
||||
import { StepContentContainer } from "./StepContentContainer";
|
||||
import { V4Badge } from "./V4Badge";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsContent,
|
||||
ClientTabsList,
|
||||
ClientTabsTrigger,
|
||||
} from "./primitives/ClientTabs";
|
||||
import { GitHubSettingsPanel } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github";
|
||||
|
||||
export function HasNoTasksDev() {
|
||||
return (
|
||||
@@ -603,7 +601,9 @@ function DeploymentOnboardingSteps() {
|
||||
<div className="mb-2 flex items-center justify-between border-b">
|
||||
<div className="mb-2 flex min-w-0 items-center gap-2">
|
||||
<EnvironmentIcon environment={environment} className="-ml-1 size-8 shrink-0" />
|
||||
<Header1 className="truncate">Deploy your tasks to {environmentFullTitle(environment)}</Header1>
|
||||
<Header1 className="truncate">
|
||||
Deploy your tasks to {environmentFullTitle(environment)}
|
||||
</Header1>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<SimpleTooltip
|
||||
@@ -655,7 +655,7 @@ function DeploymentOnboardingSteps() {
|
||||
organizationSlug={organization.slug}
|
||||
projectSlug={project.slug}
|
||||
environmentSlug={environment.slug}
|
||||
billingPath={v3BillingPath({ slug: organization.slug })}
|
||||
billingPath={v3BillingPath({ slug: organization.slug })}
|
||||
/>
|
||||
</div>
|
||||
</StepContentContainer>
|
||||
@@ -693,12 +693,16 @@ export function PromptsNone() {
|
||||
return (
|
||||
<InfoPanel
|
||||
title="Define your first prompt"
|
||||
icon={SparklesIcon}
|
||||
iconClassName="text-purple-500"
|
||||
icon={AIPromptsIcon}
|
||||
iconClassName="text-aiPrompts"
|
||||
panelClassName="max-w-lg"
|
||||
accessory={
|
||||
<LinkButton to={docsPath("prompt-management")} variant="docs/small" LeadingIcon={BookOpenIcon}>
|
||||
Prompt docs
|
||||
<LinkButton
|
||||
to={docsPath("prompt-management")}
|
||||
variant="docs/small"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
>
|
||||
Prompts docs
|
||||
</LinkButton>
|
||||
}
|
||||
>
|
||||
@@ -707,32 +711,23 @@ export function PromptsNone() {
|
||||
version them from the dashboard without redeploying.
|
||||
</Paragraph>
|
||||
<Paragraph spacing variant="small">
|
||||
Add a prompt to your project using <InlineCode variant="small">prompts.define()</InlineCode>:
|
||||
Add a prompt to your project using <InlineCode variant="small">prompts.define()</InlineCode>
|
||||
:
|
||||
</Paragraph>
|
||||
<div className="rounded border border-grid-dimmed bg-charcoal-900 p-3">
|
||||
<pre className="text-xs leading-relaxed text-text-dimmed">
|
||||
<span className="text-purple-400">import</span>
|
||||
{" { prompts } "}
|
||||
<span className="text-purple-400">from</span>
|
||||
{' "@trigger.dev/sdk";\n'}
|
||||
<span className="text-purple-400">import</span>
|
||||
{" { z } "}
|
||||
<span className="text-purple-400">from</span>
|
||||
{' "zod";\n\n'}
|
||||
<span className="text-purple-400">export const</span>
|
||||
{" myPrompt = "}
|
||||
<span className="text-blue-400">prompts.define</span>
|
||||
{"({\n"}
|
||||
{" id: "}
|
||||
<span className="text-green-400">"my-prompt"</span>
|
||||
{",\n"}
|
||||
{" variables: z.object({\n"}
|
||||
{" name: z.string(),\n"}
|
||||
{" }),\n"}
|
||||
{" content: "}
|
||||
<span className="text-green-400">{"`Hello {{name}}!`"}</span>
|
||||
{",\n"});</pre>
|
||||
</div>
|
||||
<CodeBlock
|
||||
code={`import { prompts } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
export const myPrompt = prompts.define({
|
||||
id: "my-prompt",
|
||||
variables: z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
content: \`Hello {{name}}!\`,
|
||||
});`}
|
||||
showLineNumbers={false}
|
||||
showOpenInModal={false}
|
||||
/>
|
||||
<Paragraph variant="small" className="mt-2">
|
||||
Deploy your project and your prompts will appear here with version history and a live
|
||||
editor.
|
||||
|
||||
@@ -265,7 +265,10 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn("relative overflow-hidden rounded-md border border-grid-bright", className)}
|
||||
className={cn(
|
||||
"relative flex flex-col overflow-hidden rounded-md border border-grid-bright",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: theme.plain.backgroundColor,
|
||||
}}
|
||||
@@ -355,9 +358,8 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
<div
|
||||
dir="ltr"
|
||||
className={cn(
|
||||
"px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
!isWrapped && "overflow-x-auto",
|
||||
isWrapped && "overflow-y-auto"
|
||||
"min-h-0 flex-1 px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
"overflow-auto"
|
||||
)}
|
||||
style={{
|
||||
maxHeight,
|
||||
@@ -490,9 +492,9 @@ function HighlightCode({
|
||||
}, []);
|
||||
|
||||
const containerClasses = cn(
|
||||
"px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
!isWrapped && "overflow-x-auto",
|
||||
isWrapped && "overflow-y-auto",
|
||||
"min-h-0 flex-1 px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
!isWrapped && "overflow-auto",
|
||||
isWrapped && "overflow-auto",
|
||||
className
|
||||
);
|
||||
|
||||
|
||||
@@ -20,8 +20,18 @@ export function MainBody({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
/** This container should be placed around the content on a page */
|
||||
export function PageContainer({ children }: { children: React.ReactNode }) {
|
||||
return <div className="grid h-full grid-rows-[auto_1fr] overflow-hidden">{children}</div>;
|
||||
export function PageContainer({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("grid h-full grid-rows-[auto_1fr] overflow-hidden", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageBody({
|
||||
|
||||
@@ -16,7 +16,7 @@ import { tablerIcons } from "~/utils/tablerIcons";
|
||||
import tablerSpritePath from "~/components/primitives/tabler-sprite.svg";
|
||||
import { AnthropicLogoIcon } from "~/assets/icons/AnthropicLogoIcon";
|
||||
|
||||
const shortcut = { key: "m" };
|
||||
const shortcut = { key: "l" };
|
||||
|
||||
export type ModelOption = {
|
||||
model: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { useSearchParams } from "~/hooks/useSearchParam";
|
||||
import { appliedSummary, FilterMenuProvider } from "~/components/runs/v3/SharedFilters";
|
||||
|
||||
const shortcut = { key: "v" };
|
||||
const shortcut = { key: "r" };
|
||||
|
||||
interface ProvidersFilterProps {
|
||||
possibleProviders: string[];
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Cog8ToothIcon,
|
||||
CogIcon,
|
||||
ExclamationTriangleIcon,
|
||||
PuzzlePieceIcon,
|
||||
FolderIcon,
|
||||
FolderOpenIcon,
|
||||
GlobeAmericasIcon,
|
||||
@@ -19,19 +18,20 @@ import {
|
||||
KeyIcon,
|
||||
PencilSquareIcon,
|
||||
PlusIcon,
|
||||
PuzzlePieceIcon,
|
||||
RectangleStackIcon,
|
||||
DocumentTextIcon,
|
||||
ServerStackIcon,
|
||||
SparklesIcon,
|
||||
Squares2X2Icon,
|
||||
TableCellsIcon,
|
||||
UsersIcon,
|
||||
BugAntIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Link, useFetcher, useNavigation } from "@remix-run/react";
|
||||
import { IconBugFilled } from "@tabler/icons-react";
|
||||
import { LayoutGroup, motion } from "framer-motion";
|
||||
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import simplur from "simplur";
|
||||
import { AIMetricsIcon } from "~/assets/icons/AIMetricsIcon";
|
||||
import { AIPromptsIcon } from "~/assets/icons/AIPromptsIcon";
|
||||
import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon";
|
||||
import { DropdownIcon } from "~/assets/icons/DropdownIcon";
|
||||
import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons";
|
||||
@@ -75,13 +75,13 @@ import {
|
||||
v3DeploymentsPath,
|
||||
v3EnvironmentPath,
|
||||
v3EnvironmentVariablesPath,
|
||||
v3LogsPath,
|
||||
v3ErrorsPath,
|
||||
v3PromptsPath,
|
||||
v3LogsPath,
|
||||
v3ProjectAlertsPath,
|
||||
v3ProjectPath,
|
||||
v3ProjectSettingsGeneralPath,
|
||||
v3ProjectSettingsIntegrationsPath,
|
||||
v3PromptsPath,
|
||||
v3QueuesPath,
|
||||
v3RunsPath,
|
||||
v3SchedulesPath,
|
||||
@@ -117,7 +117,6 @@ import { SideMenuHeader } from "./SideMenuHeader";
|
||||
import { SideMenuItem } from "./SideMenuItem";
|
||||
import { SideMenuSection } from "./SideMenuSection";
|
||||
import { type SideMenuSectionId } from "./sideMenuTypes";
|
||||
import { IconBugFilled } from "@tabler/icons-react";
|
||||
|
||||
/** Get the collapsed state for a specific side menu section from user preferences */
|
||||
function getSectionCollapsed(
|
||||
@@ -461,26 +460,25 @@ export function SideMenu({
|
||||
title="AI"
|
||||
isSideMenuCollapsed={isCollapsed}
|
||||
itemSpacingClassName="space-y-0"
|
||||
initialCollapsed={getSectionCollapsed(
|
||||
user.dashboardPreferences.sideMenu,
|
||||
"ai"
|
||||
)}
|
||||
initialCollapsed={getSectionCollapsed(user.dashboardPreferences.sideMenu, "ai")}
|
||||
onCollapseToggle={handleSectionToggle("ai")}
|
||||
>
|
||||
<SideMenuItem
|
||||
name="Prompts"
|
||||
icon={DocumentTextIcon}
|
||||
activeIconColor="text-purple-500"
|
||||
inactiveIconColor="text-purple-500"
|
||||
icon={AIPromptsIcon}
|
||||
trailingIconClassName="size-6"
|
||||
activeIconColor="text-aiPrompts"
|
||||
inactiveIconColor="text-aiPrompts"
|
||||
to={v3PromptsPath(organization, project, environment)}
|
||||
data-action="prompts"
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="AI Metrics"
|
||||
icon={SparklesIcon}
|
||||
activeIconColor="text-purple-500"
|
||||
inactiveIconColor="text-purple-500"
|
||||
icon={AIMetricsIcon}
|
||||
trailingIconClassName="size-5"
|
||||
activeIconColor="text-aiMetrics"
|
||||
inactiveIconColor="text-aiMetrics"
|
||||
to={v3BuiltInDashboardPath(organization, project, environment, "llm")}
|
||||
data-action="ai-metrics"
|
||||
isCollapsed={isCollapsed}
|
||||
|
||||
@@ -4,19 +4,16 @@ import React from "react";
|
||||
import { PanelGroup, Panel, PanelResizer } from "react-window-splitter";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const ResizablePanelGroup = ({ className, snapshot, ...props }: React.ComponentProps<typeof PanelGroup>) => {
|
||||
return (
|
||||
<PanelGroup
|
||||
className={cn(
|
||||
"flex w-full overflow-hidden data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
autosaveStrategy={props.autosaveId ? "cookie" : undefined}
|
||||
snapshot={snapshot}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof PanelGroup>) => (
|
||||
<PanelGroup
|
||||
className={cn(
|
||||
"flex w-full overflow-hidden data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
autosaveStrategy={props.autosaveId ? "cookie" : undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const ResizablePanel = Panel;
|
||||
|
||||
@@ -28,36 +25,42 @@ const ResizableHandle = ({
|
||||
withHandle?: boolean;
|
||||
}) => (
|
||||
<PanelResizer
|
||||
onMouseDown={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
className={cn(
|
||||
// Base styles
|
||||
"group relative flex items-center justify-center focus-custom",
|
||||
// Horizontal orientation (default)
|
||||
"w-0.75 after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2",
|
||||
// Vertical orientation
|
||||
// Horizontal size
|
||||
"w-0.75",
|
||||
// Vertical size
|
||||
"data-[handle-orientation=vertical]:h-0.75 data-[handle-orientation=vertical]:w-full",
|
||||
// Normal-state line (::before) — 1px, centered in the 3px handle
|
||||
"before:absolute before:left-px before:top-0 before:h-full before:w-px before:bg-grid-bright",
|
||||
"data-[handle-orientation=vertical]:before:left-0 data-[handle-orientation=vertical]:before:top-px data-[handle-orientation=vertical]:before:h-px data-[handle-orientation=vertical]:before:w-full",
|
||||
// Hit area (::after pseudo) for easier grabbing
|
||||
"after:absolute after:inset-y-0 after:left-1/2 after:w-3 after:-translate-x-1/2",
|
||||
"data-[handle-orientation=vertical]:after:inset-x-0 data-[handle-orientation=vertical]:after:inset-y-auto",
|
||||
"data-[handle-orientation=vertical]:after:left-0 data-[handle-orientation=vertical]:after:top-1/2",
|
||||
"data-[handle-orientation=vertical]:after:h-1 data-[handle-orientation=vertical]:after:w-full",
|
||||
"data-[handle-orientation=vertical]:after:h-3 data-[handle-orientation=vertical]:after:w-full",
|
||||
"data-[handle-orientation=vertical]:after:-translate-y-1/2 data-[handle-orientation=vertical]:after:translate-x-0",
|
||||
className
|
||||
)}
|
||||
size="3px"
|
||||
{...props}
|
||||
>
|
||||
{/* Horizontal orientation line indicator */}
|
||||
<div className="absolute left-[0.0625rem] top-0 z-20 h-full w-px bg-grid-bright transition group-hover:left-0 group-hover:w-0.75 group-hover:bg-indigo-500 group-data-[handle-orientation=vertical]:hidden" />
|
||||
{/* Vertical orientation line indicator */}
|
||||
<div className="absolute left-0 top-[0.0625rem] z-20 hidden h-px w-full bg-grid-bright transition group-hover:top-0 group-hover:h-0.75 group-hover:bg-indigo-500 group-data-[handle-orientation=vertical]:block" />
|
||||
{/* Indigo hover overlay — absolutely positioned on top of everything */}
|
||||
<div className="pointer-events-none absolute left-0 top-0 z-10 h-full w-0.75 bg-indigo-500 opacity-0 transition-opacity group-hover:opacity-100 group-data-[handle-orientation=vertical]:hidden" />
|
||||
<div className="pointer-events-none absolute left-0 top-0 z-10 hidden h-0.75 w-full bg-indigo-500 opacity-0 transition-opacity group-hover:opacity-100 group-data-[handle-orientation=vertical]:block" />
|
||||
{withHandle && (
|
||||
<>
|
||||
{/* Horizontal orientation dots (vertical arrangement) */}
|
||||
<div className="z-10 flex h-5 w-0.75 flex-col items-center justify-center gap-[0.1875rem] bg-background-dimmed group-hover:hidden group-data-[handle-orientation=vertical]:hidden">
|
||||
<div className="relative z-[1] flex h-5 w-0.75 flex-col items-center justify-center gap-[0.1875rem] bg-background-dimmed transition-opacity group-hover:opacity-0 group-data-[handle-orientation=vertical]:hidden">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="h-[0.1875rem] w-0.75 rounded-full bg-charcoal-600" />
|
||||
))}
|
||||
</div>
|
||||
{/* Vertical orientation dots (horizontal arrangement) */}
|
||||
<div className="z-10 hidden h-0.75 w-5 flex-row items-center justify-center gap-[0.1875rem] bg-background-dimmed group-hover:hidden group-data-[handle-orientation=vertical]:flex">
|
||||
<div className="relative z-[1] hidden h-0.75 w-5 flex-row items-center justify-center gap-[0.1875rem] bg-background-dimmed transition-opacity group-hover:opacity-0 group-data-[handle-orientation=vertical]:flex">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="h-0.75 w-[0.1875rem] rounded-full bg-charcoal-600" />
|
||||
))}
|
||||
|
||||
@@ -8,7 +8,7 @@ export function TextArea({ className, rows, ...props }: TextAreaProps) {
|
||||
{...props}
|
||||
rows={rows ?? 6}
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground w-full rounded border border-charcoal-800 bg-charcoal-750 px-3 text-sm text-text-bright transition focus-custom focus-custom file:border-0 file:bg-transparent file:text-base file:font-medium hover:border-charcoal-600 hover:bg-charcoal-650 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"placeholder:text-muted-foreground w-full rounded border border-charcoal-800 bg-charcoal-750 px-3 text-sm text-text-bright transition focus-custom file:border-0 file:bg-transparent file:text-base file:font-medium hover:border-charcoal-600 hover:bg-charcoal-650 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { v3PromptPath } from "~/utils/pathBuilder";
|
||||
import { TabButton, TabContainer } from "~/components/primitives/Tabs";
|
||||
import type { PromptSpanData } from "~/presenters/v3/SpanPresenter.server";
|
||||
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
|
||||
|
||||
const StreamdownRenderer = lazy(() =>
|
||||
import("streamdown").then((mod) => ({
|
||||
@@ -23,7 +24,15 @@ const StreamdownRenderer = lazy(() =>
|
||||
|
||||
type PromptTab = "overview" | "input" | "template";
|
||||
|
||||
export function PromptSpanDetails({ promptData }: { promptData: PromptSpanData }) {
|
||||
export function PromptSpanDetails({
|
||||
promptData,
|
||||
startTime,
|
||||
duration,
|
||||
}: {
|
||||
promptData: PromptSpanData;
|
||||
startTime?: string | Date;
|
||||
duration?: number | null;
|
||||
}) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const environment = useEnvironment();
|
||||
@@ -73,6 +82,7 @@ export function PromptSpanDetails({ promptData }: { promptData: PromptSpanData }
|
||||
<div className="scrollbar-gutter-stable min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
{tab === "overview" && (
|
||||
<div className="flex flex-col px-3">
|
||||
{startTime && <SpanHorizontalTimeline startTime={startTime} duration={duration ?? null} />}
|
||||
<div className="flex flex-col gap-1 py-2.5">
|
||||
<div className="flex flex-col text-xs @container">
|
||||
<MetricRow
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
subWeeks,
|
||||
} from "date-fns";
|
||||
import parse from "parse-duration";
|
||||
import { startTransition, useCallback, useEffect, useState, type ReactNode } from "react";
|
||||
import { startTransition, useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import simplur from "simplur";
|
||||
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
@@ -23,7 +23,8 @@ import { RadioButtonCircle } from "~/components/primitives/RadioButton";
|
||||
import { ComboboxProvider, SelectPopover, SelectProvider } from "~/components/primitives/Select";
|
||||
import { useOptionalOrganization } from "~/hooks/useOrganizations";
|
||||
import { useSearchParams } from "~/hooks/useSearchParam";
|
||||
import { type ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { organizationBillingPath } from "~/utils/pathBuilder";
|
||||
import { Button, LinkButton } from "../../primitives/Buttons";
|
||||
@@ -347,6 +348,8 @@ export interface TimeFilterProps {
|
||||
/** Label name used in the filter display, defaults to "Created" */
|
||||
labelName?: string;
|
||||
hideLabel?: boolean;
|
||||
/** Keyboard shortcut to open the dropdown */
|
||||
shortcut?: ShortcutDefinition;
|
||||
applyShortcut?: ShortcutDefinition | undefined;
|
||||
/** Callback when the user applies a time filter selection, receives the applied values */
|
||||
onValueChange?: (values: TimeFilterApplyValues) => void;
|
||||
@@ -363,6 +366,7 @@ export function TimeFilter({
|
||||
to,
|
||||
labelName = "Created",
|
||||
hideLabel = false,
|
||||
shortcut,
|
||||
applyShortcut,
|
||||
onValueChange,
|
||||
maxPeriodDays,
|
||||
@@ -372,6 +376,16 @@ export function TimeFilter({
|
||||
const periodValue = period ?? value("period");
|
||||
const fromValue = from ?? value("from");
|
||||
const toValue = to ?? value("to");
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useShortcutKeys({
|
||||
shortcut,
|
||||
action: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
triggerRef.current?.click();
|
||||
},
|
||||
});
|
||||
|
||||
const constrained = timeFilters({
|
||||
period: periodValue,
|
||||
@@ -386,16 +400,37 @@ export function TimeFilter({
|
||||
{() => (
|
||||
<TimeDropdown
|
||||
trigger={
|
||||
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
|
||||
<AppliedFilter
|
||||
label={hideLabel ? undefined : constrained.label}
|
||||
icon={filterIcon("period")}
|
||||
value={constrained.valueLabel}
|
||||
removable={false}
|
||||
variant="secondary/small"
|
||||
valueClassName={valueClassName}
|
||||
/>
|
||||
</Ariakit.Select>
|
||||
<Ariakit.TooltipProvider timeout={200}>
|
||||
<Ariakit.TooltipAnchor
|
||||
render={
|
||||
<Ariakit.Select
|
||||
ref={triggerRef}
|
||||
render={<div className="group cursor-pointer focus-custom" />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AppliedFilter
|
||||
label={hideLabel ? undefined : constrained.label}
|
||||
icon={filterIcon("period")}
|
||||
value={constrained.valueLabel}
|
||||
removable={false}
|
||||
variant="secondary/small"
|
||||
valueClassName={valueClassName}
|
||||
/>
|
||||
</Ariakit.TooltipAnchor>
|
||||
{shortcut && (
|
||||
<Ariakit.Tooltip className="z-40 cursor-default rounded border border-charcoal-700 bg-background-bright px-2 py-1.5 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Filter by time period</span>
|
||||
<ShortcutKey
|
||||
className="size-4 flex-none"
|
||||
shortcut={shortcut}
|
||||
variant="small"
|
||||
/>
|
||||
</div>
|
||||
</Ariakit.Tooltip>
|
||||
)}
|
||||
</Ariakit.TooltipProvider>
|
||||
}
|
||||
period={constrained.period}
|
||||
from={constrained.from}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { DateTimeAccurate } from "~/components/primitives/DateTime";
|
||||
|
||||
function formatSpanDuration(nanoseconds: number): string {
|
||||
const ms = nanoseconds / 1_000_000;
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||||
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
const totalSecs = Math.round(ms / 1000);
|
||||
const mins = Math.floor(totalSecs / 60);
|
||||
const secs = totalSecs % 60;
|
||||
return `${mins}m ${secs}s`;
|
||||
}
|
||||
|
||||
export function SpanHorizontalTimeline({
|
||||
startTime,
|
||||
duration,
|
||||
}: {
|
||||
startTime: string | Date;
|
||||
duration: number | null;
|
||||
}) {
|
||||
const startDate = startTime instanceof Date ? startTime : new Date(startTime);
|
||||
const endDate = duration != null ? new Date(startDate.getTime() + duration / 1_000_000) : null;
|
||||
|
||||
return (
|
||||
<div className="@container/timeline">
|
||||
<div className="flex flex-col gap-0.5 px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-text-bright">Started</span>
|
||||
<span className="text-xs font-medium text-text-bright">Finished</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="shrink-0 tabular-nums text-xxs text-text-dimmed @[350px]/timeline:text-xs">
|
||||
<DateTimeAccurate date={startDate} showTooltip={false} />
|
||||
</span>
|
||||
<div className="ml-2 h-3 w-px bg-charcoal-600" />
|
||||
<div className="h-px flex-1 bg-charcoal-600" />
|
||||
{duration != null && (
|
||||
<span className="shrink-0 tabular-nums px-2 text-xxs text-text-dimmed @[350px]/timeline:text-xs">
|
||||
{formatSpanDuration(duration)}
|
||||
</span>
|
||||
)}
|
||||
<div className="h-px flex-1 bg-charcoal-600" />
|
||||
<div className="mr-2 h-3 w-px bg-charcoal-600" />
|
||||
<span className="shrink-0 tabular-nums text-xxs text-text-dimmed @[350px]/timeline:text-xs">
|
||||
{endDate ? (
|
||||
<DateTimeAccurate date={endDate} previousDate={startDate} showTooltip={false} />
|
||||
) : (
|
||||
<span className="text-charcoal-500">—</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import { TaskEventStyle } from "@trigger.dev/core/v3";
|
||||
import { type TaskEventStyle } from "@trigger.dev/core/v3";
|
||||
import type { TaskEventLevel } from "@trigger.dev/database";
|
||||
import { Fragment } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
@@ -19,7 +19,7 @@ type SpanTitleProps = {
|
||||
export function SpanTitle(event: SpanTitleProps) {
|
||||
return (
|
||||
<span className={cn("flex items-center gap-x-2 overflow-x-hidden", eventTextClassName(event))}>
|
||||
<span className="truncate">{event.message}</span>{" "}
|
||||
<span className="truncate text-text-bright">{event.message}</span>{" "}
|
||||
{!event.hideAccessory && (
|
||||
<SpanAccessory accessory={event.style.accessory} size={event.size} />
|
||||
)}
|
||||
|
||||
@@ -13,12 +13,12 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { useHasAdminAccess } from "~/hooks/useUser";
|
||||
import { v3PromptPath } from "~/utils/pathBuilder";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { AIChatMessages, AssistantResponse, ChatBubble } from "./AIChatMessages";
|
||||
import type { PromptLink } from "./AIChatMessages";
|
||||
import { AIChatMessages, AssistantResponse, ChatBubble, type PromptLink } from "./AIChatMessages";
|
||||
import { AIStatsSummary, AITagsRow } from "./AIModelSummary";
|
||||
import { AIToolsInventory } from "./AIToolsInventory";
|
||||
import type { AISpanData, DisplayItem } from "./types";
|
||||
import type { PromptSpanData } from "~/presenters/v3/SpanPresenter.server";
|
||||
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
|
||||
|
||||
const StreamdownRenderer = lazy(() =>
|
||||
import("streamdown").then((mod) => ({
|
||||
@@ -36,10 +36,14 @@ export function AISpanDetails({
|
||||
aiData,
|
||||
promptVersionData,
|
||||
rawProperties,
|
||||
startTime,
|
||||
duration,
|
||||
}: {
|
||||
aiData: AISpanData;
|
||||
promptVersionData?: PromptSpanData;
|
||||
rawProperties?: string;
|
||||
startTime?: string | Date;
|
||||
duration?: number | null;
|
||||
}) {
|
||||
const [tab, setTab] = useState<AITab>("overview");
|
||||
const isAdmin = useHasAdminAccess();
|
||||
@@ -109,7 +113,12 @@ export function AISpanDetails({
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="scrollbar-gutter-stable min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
{tab === "overview" && <OverviewTab aiData={aiData} />}
|
||||
{tab === "overview" && (
|
||||
<>
|
||||
{startTime && <div className="px-3"><SpanHorizontalTimeline startTime={startTime} duration={duration ?? null} /></div>}
|
||||
<OverviewTab aiData={aiData} />
|
||||
</>
|
||||
)}
|
||||
{tab === "messages" && <MessagesTab aiData={aiData} promptLink={promptLink} />}
|
||||
{tab === "tools" && <ToolsTab aiData={aiData} />}
|
||||
{tab === "prompt" && promptVersionData && (
|
||||
|
||||
+446
-259
File diff suppressed because it is too large
Load Diff
+20
-22
@@ -6,7 +6,7 @@ import { PromptsNone } from "~/components/BlankStatePanels";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
@@ -24,7 +24,9 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
|
||||
import { PromptPresenter } from "~/presenters/v3/PromptPresenter.server";
|
||||
import { clickhouseClient } from "~/services/clickhouseInstance.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { EnvironmentParamSchema, v3PromptsPath } from "~/utils/pathBuilder";
|
||||
import { docsPath, EnvironmentParamSchema, v3PromptsPath } from "~/utils/pathBuilder";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: "Prompts | Trigger.dev" }];
|
||||
@@ -80,6 +82,11 @@ export default function PromptsPage() {
|
||||
<PageContainer>
|
||||
<NavBar>
|
||||
<PageTitle title="Prompts" />
|
||||
<PageAccessories>
|
||||
<LinkButton variant="docs/small" LeadingIcon={BookOpenIcon} to={docsPath("ai/prompts")}>
|
||||
Prompts docs
|
||||
</LinkButton>
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
<PageBody scrollable={false}>
|
||||
<Table containerClassName="border-t-0">
|
||||
@@ -108,39 +115,35 @@ export default function PromptsPage() {
|
||||
<TableCell to={path} isTabbableCell>
|
||||
<TruncatedCopyableValue value={prompt.friendlyId} />
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<span className="text-text-bright">{prompt.slug}</span>
|
||||
</TableCell>
|
||||
<TableCell to={path}>{prompt.slug}</TableCell>
|
||||
<TableCell to={path}>
|
||||
{prompt.description ? (
|
||||
<span className="text-text-dimmed" title={prompt.description.length > 80 ? prompt.description : undefined}>
|
||||
<span
|
||||
title={prompt.description.length > 80 ? prompt.description : undefined}
|
||||
>
|
||||
{prompt.description.length > 80
|
||||
? prompt.description.slice(0, 80) + "..."
|
||||
? prompt.description.slice(0, 80) + "…"
|
||||
: prompt.description}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-charcoal-500">-</span>
|
||||
<span>-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<span className="text-text-dimmed">
|
||||
{prompt.defaultModel ?? <span className="text-charcoal-500">-</span>}
|
||||
</span>
|
||||
<span>{prompt.defaultModel ?? <span>-</span>}</span>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{activeVersion ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className={`size-1.5 rounded-full ${
|
||||
isOverride ? "bg-amber-400" : "bg-green-500"
|
||||
isOverride ? "bg-warning" : "bg-success"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-text-bright">
|
||||
v{activeVersion.version}
|
||||
</span>
|
||||
<span>v{activeVersion.version}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-charcoal-500">-</span>
|
||||
<span>-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
@@ -173,12 +176,7 @@ function UsageSparkline({ data }: { data?: number[] }) {
|
||||
<div className="h-6 w-16">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData} margin={{ top: 1, right: 0, bottom: 1, left: 0 }}>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="#3b82f6"
|
||||
radius={[1, 1, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Bar dataKey="value" fill="#3b82f6" radius={[1, 1, 0, 0]} isAnimationActive={false} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
+27
-14
@@ -55,6 +55,7 @@ import { TabButton, TabContainer } from "~/components/primitives/Tabs";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { RunTimeline, RunTimelineEvent, SpanTimeline } from "~/components/run/RunTimeline";
|
||||
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
|
||||
import { PacketDisplay } from "~/components/runs/v3/PacketDisplay";
|
||||
import { RunIcon } from "~/components/runs/v3/RunIcon";
|
||||
import { RunTag } from "~/components/runs/v3/RunTag";
|
||||
@@ -289,17 +290,6 @@ function SpanBody({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isAiInspector && (
|
||||
<div className="flex items-center gap-3 pb-1.5 pl-6 text-xs text-text-dimmed">
|
||||
<DateTime date={span.startTime} includeSeconds />
|
||||
{span.duration != null && (
|
||||
<>
|
||||
<span className="text-charcoal-600">/</span>
|
||||
<span className="text-text-bright">{formatSpanDuration(span.duration)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isAiInspector ? (
|
||||
<SpanEntity span={span} />
|
||||
@@ -321,6 +311,7 @@ function formatSpanDuration(nanoseconds: number): string {
|
||||
return `${mins}m ${secs}s`;
|
||||
}
|
||||
|
||||
|
||||
function applySpanOverrides(span: Span, spanOverrides?: SpanOverride): Span {
|
||||
if (!spanOverrides) {
|
||||
return span;
|
||||
@@ -1426,17 +1417,39 @@ function SpanEntity({ span }: { span: Span }) {
|
||||
aiData={span.entity.object}
|
||||
promptVersionData={span.entity.promptVersionData}
|
||||
rawProperties={typeof span.properties === "string" ? span.properties : span.properties != null ? JSON.stringify(span.properties, null, 2) : undefined}
|
||||
startTime={span.startTime}
|
||||
duration={span.duration}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "ai-tool-call": {
|
||||
return <AIToolCallSpanDetails data={span.entity.object} />;
|
||||
return (
|
||||
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="px-3">
|
||||
<SpanHorizontalTimeline startTime={span.startTime} duration={span.duration} />
|
||||
</div>
|
||||
<AIToolCallSpanDetails data={span.entity.object} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "ai-embed": {
|
||||
return <AIEmbedSpanDetails data={span.entity.object} />;
|
||||
return (
|
||||
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="px-3">
|
||||
<SpanHorizontalTimeline startTime={span.startTime} duration={span.duration} />
|
||||
</div>
|
||||
<AIEmbedSpanDetails data={span.entity.object} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "prompt": {
|
||||
return <PromptSpanDetails promptData={span.entity.object} />;
|
||||
return (
|
||||
<PromptSpanDetails
|
||||
promptData={span.entity.object}
|
||||
startTime={span.startTime}
|
||||
duration={span.duration}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
assertNever(span.entity);
|
||||
|
||||
@@ -175,6 +175,8 @@ const projectSettings = colors.blue[500];
|
||||
const orgSettings = colors.blue[500];
|
||||
const docs = colors.blue[500];
|
||||
const bulkActions = colors.emerald[500];
|
||||
const aiPrompts = colors.blue[500];
|
||||
const aiMetrics = colors.green[500];
|
||||
|
||||
/** Other variables */
|
||||
const radius = "0.5rem";
|
||||
@@ -258,6 +260,8 @@ module.exports = {
|
||||
bulkActions,
|
||||
metrics,
|
||||
customDashboards,
|
||||
aiPrompts,
|
||||
aiMetrics,
|
||||
},
|
||||
focusStyles: {
|
||||
outline: "1px solid",
|
||||
|
||||
Reference in New Issue
Block a user