app-ui

created pr with 46.1 on 2025-02-06T15:20:57Z · by c8ef7d19
cmds
checkout latest patchset:
ssh pr.pico.sh print 46 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print 46.[rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 46

Patchset 46.1 on 2025-02-06T15:20:57Z · commit 2c47776

Install react-use-websocket
Eric Abruzzese 2025-01-15T14:28:15Z
Update the diagnostics create form to navigate to the details page with query parameters instead of POSTing to the external service
Eric Abruzzese 2025-01-15T14:29:34Z
Collect events on the diagnostic details page and construct a dashboard state
Eric Abruzzese 2025-01-15T14:30:03Z
Formatting and cleanup
Eric Abruzzese 2025-01-15T14:34:02Z
Show dashboard messages, resources, operations, and plots
Michael Peterson 2025-01-28T20:23:45Z
Support async plot annotations
Eric Abruzzese 2025-01-31T17:53:30Z
Fix an issue that would cause a page crash if datasets weren't perfectly aligned
Eric Abruzzese 2025-02-03T20:19:05Z
Add VITE_APTIBLE_AI_URL to .env.example
Eric Abruzzese 2025-02-04T16:30:13Z
Update VITE_APTIBLE_AI_URL references to point to Hotshot
Eric Abruzzese 2025-02-05T17:50:02Z
Update the analysis using PlotAnnotated events
Eric Abruzzese 2025-02-05T17:51:11Z
Install react-use-websocket
Eric Abruzzese 2025-01-15T14:28:15Z
Update the diagnostics create form to navigate to the details page with query parameters instead of POSTing to the external service
Eric Abruzzese 2025-01-15T14:29:34Z
Collect events on the diagnostic details page and construct a dashboard state
Eric Abruzzese 2025-01-15T14:30:03Z
Formatting and cleanup
Eric Abruzzese 2025-01-15T14:34:02Z
Show dashboard messages, resources, operations, and plots
Michael Peterson 2025-01-28T20:23:45Z
Support async plot annotations
Eric Abruzzese 2025-01-31T17:53:30Z
Fix an issue that would cause a page crash if datasets weren't perfectly aligned
Eric Abruzzese 2025-02-03T20:19:05Z
Add VITE_APTIBLE_AI_URL to .env.example
Eric Abruzzese 2025-02-04T16:30:13Z
Update VITE_APTIBLE_AI_URL references to point to Hotshot
Eric Abruzzese 2025-02-05T17:50:02Z
Update the analysis using PlotAnnotated events
Eric Abruzzese 2025-02-05T17:51:11Z
Refactor charts
Michael Peterson 2025-02-05T18:25:59Z
Refactor OperationsTimeline, chart hover state
Michael Peterson 2025-02-05T18:42:20Z
Extract types into aptible-ai/index, useDashboard hook
Michael Peterson 2025-02-05T19:21:56Z
Fix a linter error
Eric Abruzzese 2025-02-05T19:40:48Z
Fix linter errors
Eric Abruzzese 2025-02-05T19:50:07Z
Fix linter errors
Eric Abruzzese 2025-02-05T19:50:07Z
Semantic diff summary
0 added, 42 modified, 0 signature changed, 0 removed across 10 analyzed files
+14 -15 src/chart/chartjs-plugin-annotations.ts #
......@@ -1,9 +1,8 @@
1-import { Chart as ChartJS } from "chart.js";
2-import { type Annotation } from "@app/aptible-ai";
3-
1+import type { Annotation } from "@app/aptible-ai";
2+import type { Chart as ChartJS } from "chart.js";
43
54 // Update ChartJS interface to include annotations
6-declare module 'chart.js' {
5+declare module "chart.js" {
76 interface Chart {
87 annotationAreas?: Array<{
98 x1: number;
......@@ -21,7 +20,7 @@ declare module 'chart.js' {
2120
2221 // ChartJS plugin to draw annotations on a line chart
2322 export const annotationsPlugin = {
24- id: 'annotations',
23+ id: "annotations",
2524 afterDraw: (chart: ChartJS, args: any, options: any) => {
2625 const ctx = chart.ctx;
2726 const annotations = chart.options?.plugins?.annotations || [];
......@@ -43,17 +42,17 @@ export const annotationsPlugin = {
4342
4443 // Draw annotation rectangle
4544 ctx.save();
46- ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
45+ ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
4746 ctx.fillRect(pixelX1, pixelY1, pixelX2 - pixelX1, pixelY2 - pixelY1);
4847
4948 // Rectangle border
50- ctx.strokeStyle = 'rgb(200, 0, 0)';
49+ ctx.strokeStyle = "rgb(200, 0, 0)";
5150 ctx.strokeRect(pixelX1, pixelY1, pixelX2 - pixelX1, pixelY2 - pixelY1);
5251
5352 // Annotation label
5453 ctx.save();
5554 const padding = 4;
56- ctx.font = '10px monospace';
55+ ctx.font = "10px monospace";
5756 const textMetrics = ctx.measureText(annotation.label);
5857 const textHeight = 12;
5958 const radius = 4;
......@@ -63,25 +62,25 @@ export const annotationsPlugin = {
6362 const boxWidth = textMetrics.width + padding * 2;
6463 const boxHeight = textHeight + padding * 2;
6564
66- ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
65+ ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
6766 ctx.shadowBlur = 4;
6867 ctx.shadowOffsetX = 2;
6968 ctx.shadowOffsetY = 2;
7069
71- ctx.fillStyle = 'rgba(200, 0, 0, 0.75)';
70+ ctx.fillStyle = "rgba(200, 0, 0, 0.75)";
7271 ctx.beginPath();
7372 ctx.roundRect(boxX, boxY, boxWidth, boxHeight, radius);
7473 ctx.fill();
7574
76- ctx.shadowColor = 'transparent';
77- ctx.strokeStyle = 'rgb(200, 0, 0)';
75+ ctx.shadowColor = "transparent";
76+ ctx.strokeStyle = "rgb(200, 0, 0)";
7877 ctx.lineWidth = 1;
7978 ctx.stroke();
8079
81- ctx.fillStyle = 'white';
82- ctx.textBaseline = 'bottom';
80+ ctx.fillStyle = "white";
81+ ctx.textBaseline = "bottom";
8382 ctx.fillText(annotation.label, pixelX1 + padding * 2, pixelY1 - padding);
8483 ctx.restore();
8584 });
86- }
85+ },
8786 };
+4 -4 src/chart/chartjs-plugin-vertical-line.ts #
......@@ -1,8 +1,8 @@
1-import { Chart as ChartJS } from "chart.js";
1+import type { Chart as ChartJS } from "chart.js";
22
33 // ChartJS plugin to draw a vertical line on hover
44 export const verticalLinePlugin = {
5- id: 'verticalLine',
5+ id: "verticalLine",
66 beforeDraw: (chart: ChartJS) => {
77 if (chart.tooltip?.getActiveElements()?.length) {
88 const activePoint = chart.tooltip.getActiveElements()[0];
......@@ -16,10 +16,10 @@ export const verticalLinePlugin = {
1616 ctx.moveTo(x, topY);
1717 ctx.lineTo(x, bottomY);
1818 ctx.lineWidth = 1;
19- ctx.strokeStyle = '#94a3b8';
19+ ctx.strokeStyle = "#94a3b8";
2020 ctx.setLineDash([5, 5]);
2121 ctx.stroke();
2222 ctx.restore();
2323 }
24- }
24+ },
2525 };
+22 -10 src/ui/hooks/use-dashboard.ts #
......@@ -1,9 +1,9 @@
1-import { useEffect, useState } from "react";
2-import { useSelector } from "@app/react";
1+import type { Message, Resource } from "@app/aptible-ai";
32 import { selectAptibleAiUrl } from "@app/config";
3+import { useSelector } from "@app/react";
44 import { selectAccessToken } from "@app/token";
5+import { useEffect, useState } from "react";
56 import useWebSocket, { ReadyState } from "react-use-websocket";
6-import { type Message, type Resource } from "@app/aptible-ai";
77
88 type Dashboard = {
99 resources: {
......@@ -19,7 +19,10 @@ type UseDashboardParams = {
1919 endTime: string;
2020 };
2121
22-const handleDashboardEvent = (dashboard: Dashboard, event: Record<string, any>): Dashboard => {
22+const handleDashboardEvent = (
23+ dashboard: Dashboard,
24+ event: Record<string, any>,
25+): Dashboard => {
2326 switch (event?.type) {
2427 case "ResourceDiscovered":
2528 return {
......@@ -108,7 +111,12 @@ const handleDashboardEvent = (dashboard: Dashboard, event: Record<string, any>):
108111 }
109112 };
110113
111-export const useDashboard = ({ appId, symptomDescription, startTime, endTime }: UseDashboardParams) => {
114+export const useDashboard = ({
115+ appId,
116+ symptomDescription,
117+ startTime,
118+ endTime,
119+}: UseDashboardParams) => {
112120 const aptibleAiUrl = useSelector(selectAptibleAiUrl);
113121 const accessToken = useSelector(selectAccessToken);
114122 const [socketConnected, setSocketConnected] = useState(true);
......@@ -118,7 +126,9 @@ export const useDashboard = ({ appId, symptomDescription, startTime, endTime }:
118126 });
119127 const [hasShownCompletion, setHasShownCompletion] = useState(false);
120128
121- const { lastJsonMessage: event, readyState } = useWebSocket<Record<string, any>>(
129+ const { lastJsonMessage: event, readyState } = useWebSocket<
130+ Record<string, any>
131+ >(
122132 `${aptibleAiUrl}/troubleshoot`,
123133 {
124134 queryParams: {
......@@ -140,7 +150,9 @@ export const useDashboard = ({ appId, symptomDescription, startTime, endTime }:
140150
141151 useEffect(() => {
142152 if (event) {
143- setDashboard(prevDashboard => handleDashboardEvent(prevDashboard, event));
153+ setDashboard((prevDashboard) =>
154+ handleDashboardEvent(prevDashboard, event),
155+ );
144156 }
145157 }, [JSON.stringify(event)]);
146158
......@@ -153,9 +165,9 @@ export const useDashboard = ({ appId, symptomDescription, startTime, endTime }:
153165 messages: [
154166 ...prev.messages,
155167 {
156- id: 'completion-message',
157- severity: 'info',
158- message: 'Analysis complete.',
168+ id: "completion-message",
169+ severity: "info",
170+ message: "Analysis complete.",
159171 },
160172 ],
161173 }));
+16 -12 src/ui/pages/diagnostics-detail.tsx #
......@@ -1,12 +1,12 @@
11 import { diagnosticsCreateUrl } from "@app/routes";
22 import { useState } from "react";
33 import { useSearchParams } from "react-router-dom";
4+import { useDashboard } from "../hooks/use-dashboard";
45 import { AppSidebarLayout } from "../layouts";
56 import { Breadcrumbs } from "../shared";
67 import { HoverContext } from "../shared/diagnostics/hover";
78 import { DiagnosticsMessages } from "../shared/diagnostics/messages";
89 import { DiagnosticsResource } from "../shared/diagnostics/resource";
9-import { useDashboard } from "../hooks/use-dashboard";
1010
1111 export const DiagnosticsDetailPage = () => {
1212 const [searchParams] = useSearchParams();
......@@ -45,7 +45,9 @@ export const DiagnosticsDetailPage = () => {
4545 />
4646
4747 <div className="flex flex-col gap-4 p-4">
48- <HoverContext.Provider value={{ timestamp: hoverTimestamp, setTimestamp: setHoverTimestamp }}>
48+ <HoverContext.Provider
49+ value={{ timestamp: hoverTimestamp, setTimestamp: setHoverTimestamp }}
50+ >
4951 <DiagnosticsMessages
5052 messages={dashboard.messages}
5153 showAllMessages={showAllMessages}
......@@ -54,16 +56,18 @@ export const DiagnosticsDetailPage = () => {
5456
5557 <h2 className="text-lg font-semibold mb-2">Resources</h2>
5658 <div className="space-y-4">
57- {Object.entries(dashboard.resources).map(([resourceId, resource]) => (
58- <DiagnosticsResource
59- key={resourceId}
60- resourceId={resourceId}
61- resource={resource}
62- startTime={startTime!}
63- endTime={endTime!}
64- synchronizedHoverContext={HoverContext}
65- />
66- ))}
59+ {Object.entries(dashboard.resources).map(
60+ ([resourceId, resource]) => (
61+ <DiagnosticsResource
62+ key={resourceId}
63+ resourceId={resourceId}
64+ resource={resource}
65+ startTime={startTime}
66+ endTime={endTime}
67+ synchronizedHoverContext={HoverContext}
68+ />
69+ ),
70+ )}
6771 </div>
6872 </HoverContext.Provider>
6973 </div>
+1 -1 src/ui/shared/diagnostics/hover.tsx #
......@@ -7,5 +7,5 @@ export type HoverState = {
77
88 export const HoverContext = createContext<HoverState>({
99 timestamp: null,
10- setTimestamp: () => { },
10+ setTimestamp: () => {},
1111 });
+30 -26 src/ui/shared/diagnostics/line-chart.tsx #
......@@ -1,4 +1,3 @@
1-import React, { useContext } from "react";
21 import {
32 CategoryScale,
43 Chart as ChartJS,
......@@ -12,12 +11,13 @@ import {
1211 Title,
1312 Tooltip,
1413 } from "chart.js";
14+import React, { useContext, useEffect } from "react";
1515 import "chartjs-adapter-luxon";
16+import type { Annotation } from "@app/aptible-ai";
1617 import { Line } from "react-chartjs-2";
18+import { annotationsPlugin } from "../../../chart/chartjs-plugin-annotations";
1719 import { verticalLinePlugin } from "../../../chart/chartjs-plugin-vertical-line";
18-import { annotationsPlugin } from "../../../chart/chartjs-plugin-annoations";
19-import { type Annotation } from "@app/aptible-ai";
20-import { type HoverState } from "./hover";
20+import type { HoverState } from "./hover";
2121
2222 ChartJS.register(
2323 CategoryScale,
......@@ -30,7 +30,7 @@ ChartJS.register(
3030 Tooltip,
3131 Legend,
3232 verticalLinePlugin,
33- annotationsPlugin
33+ annotationsPlugin,
3434 );
3535
3636 export const DiagnosticsLineChart = ({
......@@ -63,14 +63,13 @@ export const DiagnosticsLineChart = ({
6363 const chartRef = React.useRef<ChartJS<"line">>();
6464
6565 // Truncate sha256 resource names to 8 chars
66- const datasets = originalDatasets.map(dataset => ({
66+ const datasets = originalDatasets.map((dataset) => ({
6767 ...dataset,
68- label: dataset.label.length === 64 ? dataset.label.slice(0, 8) : dataset.label
68+ label:
69+ dataset.label.length === 64 ? dataset.label.slice(0, 8) : dataset.label,
6970 }));
7071
71- if (!datasets || !title) return null;
72-
73- React.useEffect(() => {
72+ useEffect(() => {
7473 const chart = chartRef.current;
7574 if (!chart) return;
7675
......@@ -84,10 +83,12 @@ export const DiagnosticsLineChart = ({
8483 const timestampIndex = labels.indexOf(timestamp);
8584 if (timestampIndex === -1) return;
8685
87- const activeElements = datasets.reduce<{
88- datasetIndex: number;
89- index: number;
90- }[]>((acc, dataset, datasetIndex) => {
86+ const activeElements = datasets.reduce<
87+ {
88+ datasetIndex: number;
89+ index: number;
90+ }[]
91+ >((acc, dataset, datasetIndex) => {
9192 if (!dataset.data[timestampIndex]) return acc;
9293
9394 return [
......@@ -105,15 +106,18 @@ export const DiagnosticsLineChart = ({
105106 }, [timestamp, labels, datasets]);
106107
107108 const formatYAxisTick = (value: number, unit?: string) => {
108- if (!unit) return value;
109+ const unitStr = unit?.trim() ?? "";
110+
111+ if (!unitStr) return value;
109112
110- unit = unit.trim();
111- if (unit === '%') return `${value}%`;
112- if (unit.endsWith('B')) return `${value}${unit}`;
113+ if (unitStr === "%") return `${value}%`;
114+ if (unitStr.endsWith("B")) return `${value}${unitStr}`;
113115
114116 return value;
115117 };
116118
119+ if (!datasets || !title) return null;
120+
117121 return (
118122 <Line
119123 ref={chartRef}
......@@ -129,7 +133,7 @@ export const DiagnosticsLineChart = ({
129133 plugins: {
130134 tooltip: {
131135 enabled: true,
132- mode: 'index',
136+ mode: "index",
133137 intersect: false,
134138 },
135139 colors: {
......@@ -156,14 +160,14 @@ export const DiagnosticsLineChart = ({
156160 padding: showLegend
157161 ? undefined
158162 : {
159- top: 10,
160- bottom: 30,
161- },
163+ top: 10,
164+ bottom: 30,
165+ },
162166 },
163167 annotations: annotations,
164168 },
165169 interaction: {
166- mode: 'index',
170+ mode: "index",
167171 intersect: false,
168172 },
169173 onHover: (event, elements, chart) => {
......@@ -213,9 +217,9 @@ export const DiagnosticsLineChart = ({
213217 },
214218 title: yAxisLabel
215219 ? {
216- display: true,
217- text: yAxisLabel,
218- }
220+ display: true,
221+ text: yAxisLabel,
222+ }
219223 : undefined,
220224 ticks: {
221225 callback: (value) => formatYAxisTick(value as number, yAxisUnit),
+33 -20 src/ui/shared/diagnostics/messages.tsx #
......@@ -1,7 +1,11 @@
1+import type { Message } from "@app/aptible-ai";
12 import { StreamingText } from "../llm";
2-import { type Message } from "@app/aptible-ai";
33
4-export const DiagnosticsMessages = ({ messages, showAllMessages, setShowAllMessages }: {
4+export const DiagnosticsMessages = ({
5+ messages,
6+ showAllMessages,
7+ setShowAllMessages,
8+}: {
59 messages: Message[];
610 showAllMessages: boolean;
711 setShowAllMessages: (show: boolean) => void;
......@@ -12,33 +16,42 @@ export const DiagnosticsMessages = ({ messages, showAllMessages, setShowAllMessa
1216 <h2 className="text-lg font-semibold">Messages</h2>
1317 {messages.length > 1 && (
1418 <button
19+ type="button"
1520 onClick={() => setShowAllMessages(!showAllMessages)}
1621 className="text-blue-600 hover:text-blue-800 text-sm"
1722 >
18- {showAllMessages ? 'Show Latest' : `Show All (${messages.length})`}
23+ {showAllMessages ? "Show Latest" : `Show All (${messages.length})`}
1924 </button>
2025 )}
2126 </div>
2227 <div className="space-y-6">
23- {(showAllMessages ? messages : messages.slice(-1)).map((message, index) => (
24- <div
25- key={message.id}
26- className="flex items-start"
27- >
28- <img
29- src={message.id === 'completion-message' ? '/aptible-mark.png' : '/thinking.gif'}
30- className="w-[28px] h-[28px] mr-3"
31- aria-label="App"
32- />
33- <div className="flex-1 bg-white rounded-lg px-4 py-2 shadow-sm">
34- <StreamingText
35- text={message.message}
36- showEllipsis={(showAllMessages ? index === messages.length - 1 : true) && message.id !== 'completion-message'}
37- animate={showAllMessages ? index === messages.length - 1 : true}
28+ {(showAllMessages ? messages : messages.slice(-1)).map(
29+ (message, index) => (
30+ <div key={message.id} className="flex items-start">
31+ <img
32+ src={
33+ message.id === "completion-message"
34+ ? "/aptible-mark.png"
35+ : "/thinking.gif"
36+ }
37+ className="w-[28px] h-[28px] mr-3"
38+ aria-label="App"
3839 />
40+ <div className="flex-1 bg-white rounded-lg px-4 py-2 shadow-sm">
41+ <StreamingText
42+ text={message.message}
43+ showEllipsis={
44+ (showAllMessages ? index === messages.length - 1 : true) &&
45+ message.id !== "completion-message"
46+ }
47+ animate={
48+ showAllMessages ? index === messages.length - 1 : true
49+ }
50+ />
51+ </div>
3952 </div>
40- </div>
41- ))}
53+ ),
54+ )}
4255 </div>
4356 </div>
4457 );
+37 -22 src/ui/shared/diagnostics/operations-timeline.tsx #
......@@ -1,34 +1,42 @@
1-import React, { useRef, useContext } from "react";
2-import { type HoverState } from "./hover";
3-import { type Operation } from "@app/aptible-ai";
1+import type { Operation } from "@app/aptible-ai";
2+import type React from "react";
3+import { useContext, useRef } from "react";
4+import type { HoverState } from "./hover";
45
56 export const OperationsTimeline = ({
67 operations,
78 startTime,
89 endTime,
9- synchronizedHoverContext
10+ synchronizedHoverContext,
1011 }: {
11- operations: Operation[],
12- startTime: string,
13- endTime: string,
14- synchronizedHoverContext: React.Context<HoverState>
12+ operations: Operation[];
13+ startTime: string;
14+ endTime: string;
15+ synchronizedHoverContext: React.Context<HoverState>;
1516 }) => {
1617 const { timestamp, setTimestamp } = useContext(synchronizedHoverContext);
1718 const start = new Date(startTime);
1819 const end = new Date(endTime);
19- const minutesDiff = Math.floor((end.getTime() - start.getTime()) / (1000 * 60));
20+ const minutesDiff = Math.floor(
21+ (end.getTime() - start.getTime()) / (1000 * 60),
22+ );
2023 const timelineRef = useRef<HTMLDivElement>(null);
2124
2225 // Create array of all minutes between start and end
2326 const minutes = Array.from({ length: minutesDiff + 1 }, (_, i) => i);
2427
2528 // Map operations to their minute positions
26- const operationsByMinute = operations.reduce((acc, op) => {
27- const opTime = new Date(op.created_at);
28- const minute = Math.floor((opTime.getTime() - start.getTime()) / (1000 * 60));
29- acc[minute] = op;
30- return acc;
31- }, {} as { [key: number]: Operation });
29+ const operationsByMinute = operations.reduce(
30+ (acc, op) => {
31+ const opTime = new Date(op.created_at);
32+ const minute = Math.floor(
33+ (opTime.getTime() - start.getTime()) / (1000 * 60),
34+ );
35+ acc[minute] = op;
36+ return acc;
37+ },
38+ {} as { [key: number]: Operation },
39+ );
3240
3341 // Handle mouse move over timeline
3442 const handleMouseMove = (e: React.MouseEvent) => {
......@@ -38,14 +46,16 @@ export const OperationsTimeline = ({
3846 const x = e.clientX - rect.left;
3947 const percentage = x / rect.width;
4048 const totalMilliseconds = end.getTime() - start.getTime();
41- const hoverTime = new Date(start.getTime() + (percentage * totalMilliseconds));
49+ const hoverTime = new Date(
50+ start.getTime() + percentage * totalMilliseconds,
51+ );
4252
4353 // Round to nearest minute
4454 hoverTime.setSeconds(0);
4555 hoverTime.setMilliseconds(0);
4656
4757 // Format timestamp correctly
48- const formattedTimestamp = hoverTime.toISOString().slice(0, -5) + 'Z';
58+ const formattedTimestamp = `${hoverTime.toISOString().slice(0, -5)}Z`;
4959 setTimestamp(formattedTimestamp);
5060 };
5161
......@@ -67,7 +77,7 @@ export const OperationsTimeline = ({
6777 // Ensure position is between 0 and 100
6878 return Math.max(0, Math.min(100, position));
6979 } catch (error) {
70- console.error('Error calculating vertical line position:', error);
80+ console.error("Error calculating vertical line position:", error);
7181 return null;
7282 }
7383 };
......@@ -77,7 +87,7 @@ export const OperationsTimeline = ({
7787 // Helper function to extract operation type from description
7888 const getOperationType = (description: string) => {
7989 const match = description.match(/^\((succeeded|failed)\) (\w+)/);
80- return match ? match[2] : 'unknown';
90+ return match ? match[2] : "unknown";
8191 };
8292
8393 return (
......@@ -96,7 +106,7 @@ export const OperationsTimeline = ({
96106 className="absolute h-full w-px bg-transparent top-0"
97107 style={{
98108 left: `${verticalLinePosition}%`,
99- borderLeft: '1px dashed #94a3b8'
109+ borderLeft: "1px dashed #94a3b8",
100110 }}
101111 />
102112 )}
......@@ -114,7 +124,9 @@ export const OperationsTimeline = ({
114124 <div className="group relative">
115125 {operation ? (
116126 <>
117- <div className={`relative w-3 h-3 ${operation.status === 'succeeded' ? 'bg-lime-400' : 'bg-red-400'} rounded-full cursor-pointer before:absolute before:inset-0 before:rounded-full before:animate-ping before:opacity-75 ${operation.status === 'succeeded' ? 'before:bg-lime-400' : 'before:bg-red-400'}`} />
127+ <div
128+ className={`relative w-3 h-3 ${operation.status === "succeeded" ? "bg-lime-400" : "bg-red-400"} rounded-full cursor-pointer before:absolute before:inset-0 before:rounded-full before:animate-ping before:opacity-75 ${operation.status === "succeeded" ? "before:bg-lime-400" : "before:bg-red-400"}`}
129+ />
118130
119131 {/* Operation type label */}
120132 <div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-gray-100 px-1 rounded">
......@@ -126,7 +138,10 @@ export const OperationsTimeline = ({
126138 {/* Tooltip */}
127139 <div className="invisible group-hover:visible absolute bottom-full mb-2 -left-1/2 w-48 bg-gray-800 text-white text-sm rounded p-2 z-10">
128140 <p className="text-sm">{operation.description}</p>
129- <p className="text-xs text-gray-300">({new Date(operation.created_at).toLocaleTimeString()} local)</p>
141+ <p className="text-xs text-gray-300">
142+ ({new Date(operation.created_at).toLocaleTimeString()}{" "}
143+ local)
144+ </p>
130145 </div>
131146 </>
132147 ) : (
+26 -11 src/ui/shared/diagnostics/resource.tsx #
......@@ -1,9 +1,17 @@
1-import React from "react";
2-import { IconBox, IconCloud, IconCylinder, IconEndpoint, IconService, IconSource, IconInfo } from "../../shared/icons";
1+import type { Resource } from "@app/aptible-ai";
2+import type React from "react";
3+import {
4+ IconBox,
5+ IconCloud,
6+ IconCylinder,
7+ IconEndpoint,
8+ IconInfo,
9+ IconService,
10+ IconSource,
11+} from "../../shared/icons";
12+import type { HoverState } from "./hover";
313 import { DiagnosticsLineChart } from "./line-chart";
414 import { OperationsTimeline } from "./operations-timeline";
5-import { type HoverState } from "./hover";
6-import { type Resource } from "@app/aptible-ai";
715
816 const ResourceIcon = ({ type }: { type: Resource["type"] }) => {
917 switch (type) {
......@@ -27,7 +35,7 @@ export const DiagnosticsResource = ({
2735 resource,
2836 startTime,
2937 endTime,
30- synchronizedHoverContext
38+ synchronizedHoverContext,
3139 }: {
3240 resourceId: string;
3341 resource: Resource;
......@@ -46,7 +54,9 @@ export const DiagnosticsResource = ({
4654 {resource.operations && resource.operations.length > 0 && (
4755 <div className="mt-2">
4856 <div className="border rounded-lg bg-white shadow-sm animate-fade-in">
49- <h4 className="font-medium text-gray-900 p-3 rounded-t-lg border-b">Operations</h4>
57+ <h4 className="font-medium text-gray-900 p-3 rounded-t-lg border-b">
58+ Operations
59+ </h4>
5060 <div className="p-6">
5161 <OperationsTimeline
5262 operations={resource.operations}
......@@ -66,7 +76,9 @@ export const DiagnosticsResource = ({
6676 {Object.entries(resource.plots)
6777 .filter(([_, plot]) =>
6878 // Only show plots that have data
69- plot.series.some(series => series.points && series.points.length > 0)
79+ plot.series.some(
80+ (series) => series.points && series.points.length > 0,
81+ ),
7082 )
7183 .map(([plotId, plot]) => (
7284 <div
......@@ -96,11 +108,14 @@ export const DiagnosticsResource = ({
96108 keyId={plot.id}
97109 chart={{
98110 title: " ",
99- labels: plot.series[0]?.points.map(point => point.timestamp) || [],
100- datasets: plot.series.map(series => ({
111+ labels:
112+ plot.series[0]?.points.map(
113+ (point) => point.timestamp,
114+ ) || [],
115+ datasets: plot.series.map((series) => ({
101116 label: series.label,
102- data: series.points.map(point => point.value)
103- }))
117+ data: series.points.map((point) => point.value),
118+ })),
104119 }}
105120 xAxisUnit="minute"
106121 yAxisLabel={plot.title}
+16 -14 src/ui/shared/llm.tsx #
......@@ -3,16 +3,22 @@ import React, { useEffect, useState } from "react";
33 const TEXT_ANIMATION_INTERVAL = 150;
44 const ELLIPSIS_INTERVAL = 500;
55
6-export const StreamingText = ({ text, showEllipsis = false, animate = true }: { text: string, showEllipsis?: boolean, animate?: boolean }) => {
7- const words = text.split(' ');
8- const [visibleWords, setVisibleWords] = React.useState<number>(animate ? 0 : words.length);
6+export const StreamingText = ({
7+ text,
8+ showEllipsis = false,
9+ animate = true,
10+}: { text: string; showEllipsis?: boolean; animate?: boolean }) => {
11+ const words = text.split(" ");
12+ const [visibleWords, setVisibleWords] = React.useState<number>(
13+ animate ? 0 : words.length,
14+ );
915 const [isComplete, setIsComplete] = React.useState<boolean>(!animate);
1016
1117 React.useEffect(() => {
1218 if (!animate) return;
1319
1420 const timer = setInterval(() => {
15- setVisibleWords(prev => {
21+ setVisibleWords((prev) => {
1622 if (prev < words.length) {
1723 return prev + 1;
1824 }
......@@ -30,7 +36,7 @@ export const StreamingText = ({ text, showEllipsis = false, animate = true }: {
3036 {words.map((word, idx) => (
3137 <span
3238 key={idx}
33- className={`inline-block ${idx < words.length - 1 ? 'mr-1' : ''} ${idx < visibleWords ? '' : 'hidden'}`}
39+ className={`inline-block ${idx < words.length - 1 ? "mr-1" : ""} ${idx < visibleWords ? "" : "hidden"}`}
3440 >
3541 {word}
3642 </span>
......@@ -41,22 +47,18 @@ export const StreamingText = ({ text, showEllipsis = false, animate = true }: {
4147 };
4248
4349 export const AnimatedEllipsis = () => {
44- const [dots, setDots] = useState('');
50+ const [dots, setDots] = useState("");
4551
4652 useEffect(() => {
4753 const interval = setInterval(() => {
48- setDots(prev => {
49- if (prev === '...') return '';
50- return prev + '.';
54+ setDots((prev) => {
55+ if (prev === "...") return "";
56+ return `${prev}.`;
5157 });
5258 }, ELLIPSIS_INTERVAL);
5359
5460 return () => clearInterval(interval);
5561 }, []);
5662
57- return (
58- <span className="inline-block w-6">
59- {dots}
60- </span>
61- );
63+ return <span className="inline-block w-6">{dots}</span>;
6264 };
Back to top