diff --git a/apps/webapp/app/assets/icons/AIMetricsIcon.tsx b/apps/webapp/app/assets/icons/AIMetricsIcon.tsx new file mode 100644 index 00000000000..038eea70b49 --- /dev/null +++ b/apps/webapp/app/assets/icons/AIMetricsIcon.tsx @@ -0,0 +1,16 @@ +export function AIMetricsIcon({ className }: { className?: string }) { + return ( + + + + + ); +} diff --git a/apps/webapp/app/assets/icons/AIPromptsIcon.tsx b/apps/webapp/app/assets/icons/AIPromptsIcon.tsx new file mode 100644 index 00000000000..dd434df9931 --- /dev/null +++ b/apps/webapp/app/assets/icons/AIPromptsIcon.tsx @@ -0,0 +1,10 @@ +export function AIPromptsIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index 111e4efebb8..fe39f6785c5 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -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() {
- Deploy your tasks to {environmentFullTitle(environment)} + + Deploy your tasks to {environmentFullTitle(environment)} +
@@ -693,12 +693,16 @@ export function PromptsNone() { return ( - Prompt docs + + Prompts docs } > @@ -707,32 +711,23 @@ export function PromptsNone() { version them from the dashboard without redeploying. - Add a prompt to your project using prompts.define(): + Add a prompt to your project using prompts.define() + : -
-
-          import
-          {" { prompts } "}
-          from
-          {' "@trigger.dev/sdk";\n'}
-          import
-          {" { z } "}
-          from
-          {' "zod";\n\n'}
-          export const
-          {" myPrompt = "}
-          prompts.define
-          {"({\n"}
-          {"  id: "}
-          "my-prompt"
-          {",\n"}
-          {"  variables: z.object({\n"}
-          {"    name: z.string(),\n"}
-          {"  }),\n"}
-          {"  content: "}
-          {"`Hello {{name}}!`"}
-          {",\n"});
-
+ Deploy your project and your prompts will appear here with version history and a live editor. diff --git a/apps/webapp/app/components/code/CodeBlock.tsx b/apps/webapp/app/components/code/CodeBlock.tsx index 8757acc324e..cc28c8142d3 100644 --- a/apps/webapp/app/components/code/CodeBlock.tsx +++ b/apps/webapp/app/components/code/CodeBlock.tsx @@ -265,7 +265,10 @@ export const CodeBlock = forwardRef( return ( <>
(
{children}
; +export function PageContainer({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); } export function PageBody({ diff --git a/apps/webapp/app/components/metrics/ModelsFilter.tsx b/apps/webapp/app/components/metrics/ModelsFilter.tsx index 6250cd20c99..e641f826ae3 100644 --- a/apps/webapp/app/components/metrics/ModelsFilter.tsx +++ b/apps/webapp/app/components/metrics/ModelsFilter.tsx @@ -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; diff --git a/apps/webapp/app/components/metrics/ProvidersFilter.tsx b/apps/webapp/app/components/metrics/ProvidersFilter.tsx index f73a6f00da3..fe018eefb98 100644 --- a/apps/webapp/app/components/metrics/ProvidersFilter.tsx +++ b/apps/webapp/app/components/metrics/ProvidersFilter.tsx @@ -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[]; diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 1a1521bc68b..18a4387c996 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -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")} > ) => { - return ( - - ); -}; +const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps) => ( + +); const ResizablePanel = Panel; @@ -28,36 +25,42 @@ const ResizableHandle = ({ withHandle?: boolean; }) => ( { + 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 */} -
- {/* Vertical orientation line indicator */} -
+ {/* Indigo hover overlay — absolutely positioned on top of everything */} +
+
{withHandle && ( <> {/* Horizontal orientation dots (vertical arrangement) */} -
+
{Array.from({ length: 3 }).map((_, index) => (
))}
{/* Vertical orientation dots (horizontal arrangement) */} -
+
{Array.from({ length: 3 }).map((_, index) => (
))} diff --git a/apps/webapp/app/components/primitives/TextArea.tsx b/apps/webapp/app/components/primitives/TextArea.tsx index f5350a510bc..06dc84622da 100644 --- a/apps/webapp/app/components/primitives/TextArea.tsx +++ b/apps/webapp/app/components/primitives/TextArea.tsx @@ -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 )} /> diff --git a/apps/webapp/app/components/runs/v3/PromptSpanDetails.tsx b/apps/webapp/app/components/runs/v3/PromptSpanDetails.tsx index 304f47220b7..9645087b859 100644 --- a/apps/webapp/app/components/runs/v3/PromptSpanDetails.tsx +++ b/apps/webapp/app/components/runs/v3/PromptSpanDetails.tsx @@ -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 }
{tab === "overview" && (
+ {startTime && }
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(null); + + useShortcutKeys({ + shortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); const constrained = timeFilters({ period: periodValue, @@ -386,16 +400,37 @@ export function TimeFilter({ {() => ( }> - - + + } + /> + } + > + + + {shortcut && ( + +
+ Filter by time period + +
+
+ )} +
} period={constrained.period} from={constrained.from} diff --git a/apps/webapp/app/components/runs/v3/SpanHorizontalTimeline.tsx b/apps/webapp/app/components/runs/v3/SpanHorizontalTimeline.tsx new file mode 100644 index 00000000000..b4acf2a1e7b --- /dev/null +++ b/apps/webapp/app/components/runs/v3/SpanHorizontalTimeline.tsx @@ -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 ( +
+
+
+ Started + Finished +
+
+ + + +
+
+ {duration != null && ( + + {formatSpanDuration(duration)} + + )} +
+
+ + {endDate ? ( + + ) : ( + + )} + +
+
+
+ ); +} diff --git a/apps/webapp/app/components/runs/v3/SpanTitle.tsx b/apps/webapp/app/components/runs/v3/SpanTitle.tsx index 2be7bfdcbdd..c54b93cb690 100644 --- a/apps/webapp/app/components/runs/v3/SpanTitle.tsx +++ b/apps/webapp/app/components/runs/v3/SpanTitle.tsx @@ -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 ( - {event.message}{" "} + {event.message}{" "} {!event.hideAccessory && ( )} diff --git a/apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx b/apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx index c2fc4df6069..5e8bb65688f 100644 --- a/apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx +++ b/apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx @@ -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("overview"); const isAdmin = useHasAdminAccess(); @@ -109,7 +113,12 @@ export function AISpanDetails({ {/* Tab content */}
- {tab === "overview" && } + {tab === "overview" && ( + <> + {startTime &&
} + + + )} {tab === "messages" && } {tab === "tools" && } {tab === "prompt" && promptVersionData && ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx index dc84de22bae..5a953c0199b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx @@ -1,5 +1,6 @@ import * as Ariakit from "@ariakit/react"; -import { ArrowPathIcon, SparklesIcon } from "@heroicons/react/20/solid"; +import { ArrowPathIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid"; +import { DialogClose } from "@radix-ui/react-dialog"; import { type MetaFunction, useFetcher } from "@remix-run/react"; import { type ActionFunctionArgs, @@ -7,35 +8,47 @@ import { type LoaderFunctionArgs, redirect, } from "@remix-run/server-runtime"; -import { DialogClose } from "@radix-ui/react-dialog"; +import { AnimatePresence, motion } from "framer-motion"; +import { ClipboardCheckIcon, ClipboardIcon, GitBranchPlusIcon } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { CodeBlock } from "~/components/code/CodeBlock"; import { TextEditor } from "~/components/code/TextEditor"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { ModelsFilter } from "~/components/metrics/ModelsFilter"; +import { OperationsFilter } from "~/components/metrics/OperationsFilter"; +import { ProvidersFilter } from "~/components/metrics/ProvidersFilter"; +import { AppliedFilter } from "~/components/primitives/AppliedFilter"; import { Badge } from "~/components/primitives/Badge"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; -import { Button } from "~/components/primitives/Buttons"; -import { Header2, Header3 } from "~/components/primitives/Headers"; import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog"; +import { Header3 } from "~/components/primitives/Headers"; import { Hint } from "~/components/primitives/Hint"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; -import { Spinner } from "~/components/primitives/Spinner"; +import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover"; import * as Property from "~/components/primitives/PropertyTable"; +import { + Table, + TableBody, + TableCell, + TableCellMenu, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; +import { RadioButtonCircle } from "~/components/primitives/RadioButton"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, type ResizableSnapshot, } from "~/components/primitives/Resizable"; -import { TabButton, TabContainer } from "~/components/primitives/Tabs"; -import { CopyButton } from "~/components/primitives/CopyButton"; -import { CopyableText } from "~/components/primitives/CopyableText"; import { SelectItem, SelectList, @@ -43,35 +56,35 @@ import { SelectProvider, SelectTrigger, } from "~/components/primitives/Select"; +import { Spinner } from "~/components/primitives/Spinner"; +import { TabButton, TabContainer } from "~/components/primitives/Tabs"; import { TextArea } from "~/components/primitives/TextArea"; -import { ModelsFilter } from "~/components/metrics/ModelsFilter"; -import { OperationsFilter } from "~/components/metrics/OperationsFilter"; -import { ProvidersFilter } from "~/components/metrics/ProvidersFilter"; -import { AppliedFilter } from "~/components/primitives/AppliedFilter"; -import tablerSpritePath from "~/components/primitives/tabler-sprite.svg"; import { TimeFilter } from "~/components/runs/v3/SharedFilters"; -import { SpanView } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route"; +import { prisma } from "~/db.server"; import { useEnvironment } from "~/hooks/useEnvironment"; +import { useInterval } from "~/hooks/useInterval"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { prisma } from "~/db.server"; -import { clickhouseClient } from "~/services/clickhouseInstance.server"; -import { useInterval } from "~/hooks/useInterval"; import { useSearchParams } from "~/hooks/useSearchParam"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; -import { PromptPresenter, type GenerationRow } from "~/presenters/v3/PromptPresenter.server"; +import { type GenerationRow, PromptPresenter } from "~/presenters/v3/PromptPresenter.server"; +import { SpanView } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route"; +import { clickhouseClient } from "~/services/clickhouseInstance.server"; import { getResizableSnapshot } from "~/services/resizablePanel.server"; import { requireUserId } from "~/services/session.server"; import { PromptService } from "~/v3/services/promptService.server"; -import { MetricWidget } from "~/routes/resources.metric"; -import { InfoPanel } from "~/components/primitives/InfoPanel"; +import { z } from "zod"; +import { AIPromptsIcon } from "~/assets/icons/AIPromptsIcon"; +import { RunsIcon } from "~/assets/icons/RunsIcon"; import { InlineCode } from "~/components/code/InlineCode"; -import { TextLink } from "~/components/primitives/TextLink"; +import { InfoPanel } from "~/components/primitives/InfoPanel"; +import { SimpleTooltip } from "~/components/primitives/Tooltip"; +import { MetricWidget } from "~/routes/resources.metric"; +import { cn } from "~/utils/cn"; import { EnvironmentParamSchema, v3PromptsPath, v3RunSpanPath } from "~/utils/pathBuilder"; import { parsePeriodToMs } from "~/utils/periods"; -import { z } from "zod"; const ParamSchema = EnvironmentParamSchema.extend({ promptSlug: z.string(), @@ -233,7 +246,11 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { let generations: Awaited>["generations"] = []; let generationsPagination: { next?: string } = {}; try { - const urlVersions = url.searchParams.getAll("versions").filter(Boolean).map(Number).filter((n) => !isNaN(n)); + const urlVersions = url.searchParams + .getAll("versions") + .filter(Boolean) + .map(Number) + .filter((n) => !isNaN(n)); const urlModels = url.searchParams.getAll("models").filter(Boolean); const urlOperations = url.searchParams.getAll("operations").filter(Boolean); const urlProviders = url.searchParams.getAll("providers").filter(Boolean); @@ -284,7 +301,11 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const possibleProviders = provsErr ? [] : provsRows.map((r) => r.val); return typedjson({ - resizable: { outer: resizableOuter, vertical: resizableVertical, generations: resizableGenerations }, + resizable: { + outer: resizableOuter, + vertical: resizableVertical, + generations: resizableGenerations, + }, prompt: { id: prompt.id, friendlyId: prompt.friendlyId, @@ -460,33 +481,44 @@ export default function PromptDetailPage() { }; return ( - + - {prompt.slug} - - - -
+ } backButton={{ to: v3PromptsPath(organization, project, environment), text: "Prompts" }} />
{selectedVersion && ( - - v{selectedVersion.version} - {isCurrent && current} +
+
+ v{selectedVersion.version} + {isCurrent && current} {selectedVersion.labels.includes("override") && ( - override + + override + )} - +
)} {selectedVersion && !isCurrent && selectedVersion.source === "code" && ( )} {!overrideVersion && ( - )}
- {overrideVersion && ( -
- - Override v{overrideVersion.version} is active. API calls resolve this version instead of the deployed prompt. - -
- - -
-
- )} + + Override v{overrideVersion.version} is active. API calls resolve to this version + instead of the deployed prompt. + +
+ + +
+ + )} + +
{/* Template panel */} -
- {/* Sticky header */} -
-
- Template - {content && } -
+ {content ? ( + + ) : ( +
+ + No content +
- {/* Scrollable content */} - {content ? ( -
- -
- ) : ( -
- - No content - -
- )} -
+ )} @@ -593,8 +626,8 @@ export default function PromptDetailPage() {
{/* Tab bar */} -
- +
+ -
+
({ model: m, system: "" }))} @@ -640,12 +673,18 @@ export default function PromptDetailPage() { labelName="Period" hideLabel valueClassName="text-text-bright" + shortcut={{ key: "w" }} />
{/* Tab content */} -
+
{contentTab === "generations" && ( {/* Sidebar */} - +
{/* Tabs */}
@@ -696,7 +741,7 @@ export default function PromptDetailPage() { isActive={tab === "preview"} layoutId="prompt-sidebar" onClick={() => replaceSearch({ tab: "preview", version: versionParam })} - shortcut={{ key: "p" }} + shortcut={{ key: "i" }} > Preview @@ -712,8 +757,15 @@ export default function PromptDetailPage() {
{/* Tab content */} -
- {tab === "details" && } +
+ {tab === "details" && ( + + )} {tab === "preview" && } {tab === "versions" && ( v.id === overrideVersion.id) ?? { textContent: null }) : content} + content={ + overrideVersion + ? getVersionContent( + versions.find((v) => v.id === overrideVersion.id) ?? { textContent: null } + ) + : content + } isEditingOverride={!!overrideVersion} - currentOverrideModel={overrideVersion ? versions.find((v) => v.id === overrideVersion.id)?.model ?? null : null} + currentOverrideModel={ + overrideVersion ? versions.find((v) => v.id === overrideVersion.id)?.model ?? null : null + } onSave={(textContent, commitMessage, model) => { const intent = overrideVersion ? "updateOverride" : "saveVersion"; fetcher.submit({ intent, textContent, commitMessage, model }, { method: "POST" }); @@ -804,7 +864,7 @@ function OverrideDialog({ return ( - + {isEditingOverride ? "Edit override" : "Create override"} @@ -814,7 +874,7 @@ function OverrideDialog({ className="-mx-3 w-auto flex-1 border-b border-t border-grid-dimmed" > {/* Editor */} - + {/* Footer */} -
+
- +