From c002decfe50ae7249b2d8243e0efc828e78f9155 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 30 May 2025 23:45:48 -0400 Subject: [PATCH] Show tasks in more of a card view (#56) * Show tasks in more of a card view * PR feedback --- src/actions/analytics/events.ts | 48 ++++--- src/app/(authenticated)/usage/TaskDrawer.tsx | 8 +- src/app/(authenticated)/usage/Tasks.tsx | 125 ++++++------------- src/app/(authenticated)/usage/Usage.tsx | 7 +- src/components/usage/TaskCard.tsx | 83 ++++++++++++ src/components/usage/index.ts | 1 + src/lib/taskUtils.ts | 22 ++++ 7 files changed, 181 insertions(+), 113 deletions(-) create mode 100644 src/components/usage/TaskCard.tsx create mode 100644 src/lib/taskUtils.ts diff --git a/src/actions/analytics/events.ts b/src/actions/analytics/events.ts index 98989f2243..269e506e23 100644 --- a/src/actions/analytics/events.ts +++ b/src/actions/analytics/events.ts @@ -8,7 +8,7 @@ import { } from '@roo-code/types'; import type { TimePeriod } from '@/types'; -import { type Task, taskSchema } from '@/types/analytics'; +import { taskSchema } from '@/types/analytics'; import { analytics } from '@/lib/server'; import { type User, getUsersById } from '@/db/server'; @@ -225,30 +225,48 @@ export const getModelUsage = async ({ * getTasks */ +const taskWithTitleSchema = taskSchema.extend({ + title: z.string().nullable(), +}); + +export type TaskWithTitle = z.infer; + +export type TaskWithUser = TaskWithTitle & { user: User }; + export const getTasks = async ({ orgId, }: { orgId?: string | null; -}): Promise<(Task & { user: User })[]> => { +}): Promise => { if (!orgId) { return []; } const results = await analytics.query({ query: ` + WITH first_messages AS ( + SELECT + taskId, + argMin(text, ts) as title + FROM messages + WHERE orgId = {orgId: String} + GROUP BY taskId + ) SELECT - taskId, - userId, - apiProvider AS provider, - modelId as model, - MAX(CASE WHEN type = 'Task Completed' THEN 1 ELSE 0 END) AS completed, - SUM(CASE WHEN type = 'LLM Completion' THEN COALESCE(inputTokens, 0) + COALESCE(outputTokens, 0) ELSE 0 END) AS tokens, - SUM(CASE WHEN type = 'LLM Completion' THEN COALESCE(cost, 0) ELSE 0 END) AS cost, - MIN(timestamp) AS timestamp - FROM events + e.taskId, + e.userId, + e.apiProvider AS provider, + e.modelId as model, + MAX(CASE WHEN e.type = 'Task Completed' THEN 1 ELSE 0 END) AS completed, + SUM(CASE WHEN e.type = 'LLM Completion' THEN COALESCE(e.inputTokens, 0) + COALESCE(e.outputTokens, 0) ELSE 0 END) AS tokens, + SUM(CASE WHEN e.type = 'LLM Completion' THEN COALESCE(e.cost, 0) ELSE 0 END) AS cost, + MIN(e.timestamp) AS timestamp, + any(fm.title) AS title + FROM events e + LEFT JOIN first_messages fm ON e.taskId = fm.taskId WHERE - orgId = {orgId: String} - AND type IN ({types: Array(String)}) + e.orgId = {orgId: String} + AND e.type IN ({types: Array(String)}) GROUP BY 1, 2, 3, 4 ORDER BY timestamp DESC `, @@ -263,11 +281,11 @@ export const getTasks = async ({ }, }); - const tasks = z.array(taskSchema).parse(await results.json()); + const tasks = z.array(taskWithTitleSchema).parse(await results.json()); const users = await getUsersById(tasks.map(({ userId }) => userId)); return tasks .map((usage) => ({ ...usage, user: users[usage.userId] })) - .filter((usage): usage is Task & { user: User } => !!usage.user); + .filter((usage): usage is TaskWithUser => !!usage.user); }; diff --git a/src/app/(authenticated)/usage/TaskDrawer.tsx b/src/app/(authenticated)/usage/TaskDrawer.tsx index 7d34ceec90..3b5e18e57a 100644 --- a/src/app/(authenticated)/usage/TaskDrawer.tsx +++ b/src/app/(authenticated)/usage/TaskDrawer.tsx @@ -1,10 +1,10 @@ import { useQuery } from '@tanstack/react-query'; import { X } from 'lucide-react'; -import type { User } from '@/db/server'; -import type { Task } from '@/types/analytics'; +import type { TaskWithUser } from '@/actions/analytics'; import { getMessages } from '@/actions/analytics'; import { formatCurrency, formatNumber } from '@/lib/formatters'; +import { generateFallbackTitle } from '@/lib/taskUtils'; import { Drawer, DrawerContent, @@ -18,7 +18,7 @@ import { Status } from './Status'; import { Messages } from './Messages'; type TaskDrawerProps = { - task: Task & { user: User }; + task: TaskWithUser; onClose: () => void; }; @@ -32,7 +32,7 @@ export const TaskDrawer = ({ task, onClose }: TaskDrawerProps) => { - Task + {task.title || generateFallbackTitle(task)} {task.taskId}
- ), - }, - { - header: 'Developer', - cell: ({ row }) => ( - - ), - }, - { - header: 'Model', - cell: ({ row }) => ( - - ), - }, - { - header: 'Tokens', - cell: ({ row }) => formatNumber(row.original.tokens), - }, - { - header: 'Cost (USD)', - cell: ({ row }) => formatCurrency(row.original.cost), - }, - { - header: 'Date', - cell: ({ row }) => - new Date(1000 * row.original.timestamp).toLocaleString(), - }, - { - header: 'Status', - cell: ({ row }) => , - }, - ], - [onFilter, onTaskSelected], - ); + if (isPending) { + return ( +
+ {Array(8) + .fill(0) + .map((_, i) => ( + + ))} +
+ ); + } - const columns = useMemo( - () => - isPending - ? cols.map((col) => ({ - ...col, - cell: () => , - })) - : cols, - [isPending, cols], - ); + if (tasks.length === 0) { + return ( +
+ No tasks found +
+ ); + } - return ; + return ( +
+ {tasks.map((task) => ( + + ))} +
+ ); }; diff --git a/src/app/(authenticated)/usage/Usage.tsx b/src/app/(authenticated)/usage/Usage.tsx index f3e0c33715..02c4483c36 100644 --- a/src/app/(authenticated)/usage/Usage.tsx +++ b/src/app/(authenticated)/usage/Usage.tsx @@ -4,8 +4,7 @@ import { useState, useCallback } from 'react'; import { useTranslations } from 'next-intl'; import { X } from 'lucide-react'; -import type { User } from '@/db/server'; -import type { Task } from '@/types/analytics'; +import type { TaskWithUser } from '@/actions/analytics'; import { Badge, Button } from '@/components/ui'; import { UsageCard } from '@/components/usage'; @@ -19,7 +18,7 @@ export const Usage = () => { const t = useTranslations('Analytics'); const [viewMode, setViewMode] = useState('tasks'); const [filter, setFilter] = useState(null); - const [task, setTask] = useState<(Task & { user: User }) | null>(null); + const [task, setTask] = useState(null); const onFilter = useCallback((filter: Filter) => { setFilter(filter); @@ -62,7 +61,7 @@ export const Usage = () => { setTask(task)} + onTaskSelected={(task: TaskWithUser) => setTask(task)} /> ) : viewMode === 'developers' ? ( diff --git a/src/components/usage/TaskCard.tsx b/src/components/usage/TaskCard.tsx new file mode 100644 index 0000000000..864ede4427 --- /dev/null +++ b/src/components/usage/TaskCard.tsx @@ -0,0 +1,83 @@ +import { + formatNumber, + formatCurrency, + formatTimestamp, +} from '@/lib/formatters'; +import { generateFallbackTitle } from '@/lib/taskUtils'; +import { Card, CardContent, Button } from '@/components/ui'; + +import type { TaskWithUser } from '@/actions/analytics'; +import type { Filter } from '@/app/(authenticated)/usage/types'; +import { Status } from '@/app/(authenticated)/usage/Status'; + +type TaskCardProps = { + task: TaskWithUser; + onFilter: (filter: Filter) => void; + onTaskSelected: (task: TaskWithUser) => void; +}; + +export const TaskCard = ({ task, onFilter, onTaskSelected }: TaskCardProps) => { + return ( + onTaskSelected(task)} + > + +
+ {/* First line - Title and Status */} +
+

+ {task.title || generateFallbackTitle(task)} +

+ +
+ + {/* Second line - All metadata */} +
+
+ {formatTimestamp(task.timestamp)} + + +
+ + {/* Metrics on the right */} +
+ + {formatNumber(task.tokens)} tokens + + {formatCurrency(task.cost)} +
+
+
+
+
+ ); +}; diff --git a/src/components/usage/index.ts b/src/components/usage/index.ts index aa3a3a7fe2..f273b86c61 100644 --- a/src/components/usage/index.ts +++ b/src/components/usage/index.ts @@ -1 +1,2 @@ export { UsageCard } from './UsageCard'; +export { TaskCard } from './TaskCard'; diff --git a/src/lib/taskUtils.ts b/src/lib/taskUtils.ts new file mode 100644 index 0000000000..7caa9e798b --- /dev/null +++ b/src/lib/taskUtils.ts @@ -0,0 +1,22 @@ +import { format, getHours } from 'date-fns'; +import type { TaskWithUser } from '@/actions/analytics'; + +export const generateFallbackTitle = (task: TaskWithUser) => { + const date = new Date(task.timestamp * 1000); + const dayOfWeek = format(date, 'EEEE'); // Full day name (e.g., 'Monday') + const hour = getHours(date); + + // Determine time of day + let timeOfDay; + if (hour >= 5 && hour < 12) { + timeOfDay = 'morning'; + } else if (hour >= 12 && hour < 17) { + timeOfDay = 'afternoon'; + } else if (hour >= 17 && hour < 21) { + timeOfDay = 'evening'; + } else { + timeOfDay = 'night'; + } + + return `${dayOfWeek} ${timeOfDay} task`; +};