Skip to content

Commit

Permalink
feat: add agent activity
Browse files Browse the repository at this point in the history
  • Loading branch information
Tanya-atatakai committed Dec 13, 2024
1 parent d366bfb commit 80d51b9
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 2 deletions.
20 changes: 20 additions & 0 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,26 @@ const createMainWindow = async () => {
}
});

// Get the agent's current state
ipcMain.handle('health-check', async (_event) => {
try {
const response = await fetch('http://127.0.0.1:8716/healthcheck', {
method: 'GET',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});

if (!response.ok) {
throw new Error('Failed to fetch health check');
}

const data = await response.json();
return { response: data };
} catch (error) {
console.error('Health check error:', error);
return { error: error.message };
}
});

mainWindow.webContents.on('did-fail-load', () => {
mainWindow.webContents.reloadIgnoringCache();
});
Expand Down
1 change: 1 addition & 0 deletions electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
getAppVersion: () => ipcRenderer.invoke('app-version'),
validateTwitterLogin: (credentials) =>
ipcRenderer.invoke('validate-twitter-login', credentials),
healthCheck: () => ipcRenderer.invoke('health-check'),
});
153 changes: 153 additions & 0 deletions frontend/components/AgentActivity/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { ApiOutlined, CloseOutlined, InboxOutlined } from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query';
import { Button, Card, Collapse, Flex, Spin, Typography } from 'antd';
import { isEmpty, isNull } from 'lodash';
import { CSSProperties, ReactNode, useContext, useMemo } from 'react';

import { COLOR } from '@/constants/colors';
import { FIVE_SECONDS_INTERVAL } from '@/constants/intervals';
import { REACT_QUERY_KEYS } from '@/constants/react-query-keys';
import { NA } from '@/constants/symbols';
import { OnlineStatusContext } from '@/context/OnlineStatusProvider';
import { Pages } from '@/enums/Pages';
import { useElectronApi } from '@/hooks/useElectronApi';
import { usePageState } from '@/hooks/usePageState';
import { useService } from '@/hooks/useService';
import { useServices } from '@/hooks/useServices';

import { CardTitle } from '../Card/CardTitle';

const { Text } = Typography;

const MIN_HEIGHT = 400;
const ICON_STYLE: CSSProperties = { fontSize: 48, color: COLOR.TEXT_LIGHT };
const CURRENT_ACTIVITY_STYLE: CSSProperties = {
background: 'linear-gradient(180deg, #FCFCFD 0%, #EEF0F7 100%)',
};

const Container = ({ children }: { children: ReactNode }) => (
<Flex
vertical
gap={16}
align="center"
justify="center"
style={{ height: MIN_HEIGHT }}
>
{children}
</Flex>
);

const Loading = () => (
<Container>
<Spin />
</Container>
);

const ErrorLoadingData = ({ refetch }: { refetch: () => void }) => (
<Container>
<ApiOutlined style={ICON_STYLE} />
<Text type="secondary">Error loading data</Text>
<Button onClick={refetch}>Try again</Button>
</Container>
);

const NoData = () => (
<Container>
<InboxOutlined style={ICON_STYLE} />
<Text type="secondary">
There&apos;s no previous agent activity recorded yet
</Text>
</Container>
);

export const AgentActivityPage = () => {
const electronApi = useElectronApi();
const { isOnline } = useContext(OnlineStatusContext);
const { goto } = usePageState();

const { selectedService } = useServices();
const { isServiceRunning } = useService(selectedService?.service_config_id);

const { data, isLoading, isError, refetch } = useQuery({
queryKey: REACT_QUERY_KEYS.AGENT_ACTIVITY,
queryFn: async () => {
const result = await electronApi?.healthCheck?.();
if (result && 'error' in result) throw new Error(result.error);
return result;
},
select: (data) => {
if (!data || !('response' in data) || !data.response) return null;

// The latest activity should go at the top, so sort the rounds accordingly
const rounds = [...(data.response?.rounds || [])].reverse();
const roundsInfo = data.response?.rounds_info;
return { rounds, roundsInfo };
},
enabled: isServiceRunning,
refetchOnWindowFocus: false,
refetchInterval: (query) => {
if (query.state.error) return false; // Stop refetching when there's an error
return isOnline ? FIVE_SECONDS_INTERVAL : false;
},
});

const activity = useMemo(() => {
if (isLoading) return <Loading />;
if (isError) return <ErrorLoadingData refetch={refetch} />;
if (!isServiceRunning) return <NoData />;
if (isNull(data) || isEmpty(data)) return <NoData />;

const items = data.rounds.map((item, index) => {
const isCurrent = index === 0;
const roundName = data.roundsInfo?.[item]?.name || item;
return {
key: `${item}-${index}`,
label: isCurrent ? (
<Flex vertical gap={4}>
<Text type="secondary" className="text-xs">
Current activity
</Text>
<Text className="text-sm loading-ellipses">{roundName}</Text>
</Flex>
) : (
<Text className="text-sm">{roundName}</Text>
),
children: (
<Text
type="secondary"
className="text-sm"
style={{ marginLeft: '26px' }}
>
{data.roundsInfo?.[item]?.description || NA}
</Text>
),
style: isCurrent ? CURRENT_ACTIVITY_STYLE : undefined,
};
});

return (
<Collapse
items={items}
bordered={false}
style={{ background: 'transparent' }}
/>
);
}, [data, isError, isLoading, isServiceRunning, refetch]);

return (
<Card
title={<CardTitle title="Agent activity" />}
bordered={false}
styles={{ body: { padding: '1px 0 24px' } }}
extra={
<Button
size="large"
icon={<CloseOutlined />}
onClick={() => goto(Pages.Main)}
/>
}
>
{activity}
</Card>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useServices } from '@/hooks/useServices';
import { ServicesService } from '@/service/Services';

import { LastTransaction } from '../LastTransaction';
import { WhatIsAgentDoing } from '../WhatIsAgentDoing';

const { Paragraph, Text } = Typography;

Expand All @@ -30,7 +31,10 @@ const IdleTooltip = () => (
);

export const AgentRunningButton = () => {
const isLastTransactionEnabled = useFeatureFlag('last-transactions');
const [isLastTransactionEnabled, isAgentActivityEnabled] = useFeatureFlag([
'last-transactions',
'agent-activity',
]);
const { showNotification } = useElectronApi();
const { isEligibleForRewards } = useReward();

Expand Down Expand Up @@ -72,7 +76,7 @@ export const AgentRunningButton = () => {
Pause
</Button>

<Flex vertical>
<Flex vertical align="start">
{isEligibleForRewards ? (
<Text type="secondary" className="text-sm">
Agent is idle&nbsp;
Expand All @@ -87,6 +91,8 @@ export const AgentRunningButton = () => {
{isLastTransactionEnabled && (
<LastTransaction serviceConfigId={serviceConfigId} />
)}

{isAgentActivityEnabled && <WhatIsAgentDoing />}
</Flex>
</Flex>
);
Expand Down
17 changes: 17 additions & 0 deletions frontend/components/MainPage/header/WhatIsAgentDoing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Button } from 'antd';

import { Pages } from '@/enums/Pages';
import { usePageState } from '@/hooks/usePageState';

export const WhatIsAgentDoing = () => {
const { goto } = usePageState();
return (
<Button
type="link"
className="p-0 text-xs"
onClick={() => goto(Pages.AgentActivity)}
>
What&apos;s my agent doing?
</Button>
);
};
3 changes: 3 additions & 0 deletions frontend/constants/react-query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,7 @@ export const REACT_QUERY_KEYS = {
'getOwners',
multisigs.map((multisig) => multisig.address),
] as const,

// agent activity
AGENT_ACTIVITY: ['agentActivity'] as const,
} as const;
6 changes: 6 additions & 0 deletions frontend/context/ElectronApiProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { get } from 'lodash';
import { createContext, PropsWithChildren } from 'react';

import { AgentHealthCheck } from '@/types/Agent';
import { ElectronStore, ElectronTrayIconStatus } from '@/types/ElectronApi';

type ElectronApiContextProps = {
Expand Down Expand Up @@ -41,6 +42,9 @@ type ElectronApiContextProps = {
password: string;
email: string;
}) => Promise<{ success: boolean }>;
healthCheck?: () => Promise<
{ response: AgentHealthCheck | null } | { error: string }
>;
};

export const ElectronApiContext = createContext<ElectronApiContextProps>({
Expand All @@ -65,6 +69,7 @@ export const ElectronApiContext = createContext<ElectronApiContextProps>({
saveLogs: async () => ({ success: false }),
openPath: () => {},
validateTwitterLogin: async () => ({ success: false }),
healthCheck: async () => ({ response: null }),
});

export const ElectronApiProvider = ({ children }: PropsWithChildren) => {
Expand Down Expand Up @@ -106,6 +111,7 @@ export const ElectronApiProvider = ({ children }: PropsWithChildren) => {
saveLogs: getElectronApiFunction('saveLogs'),
openPath: getElectronApiFunction('openPath'),
validateTwitterLogin: getElectronApiFunction('validateTwitterLogin'),
healthCheck: getElectronApiFunction('healthCheck'),
}}
>
{children}
Expand Down
1 change: 1 addition & 0 deletions frontend/enums/Pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export enum Pages {
RewardsHistory,
AddBackupWalletViaSafe,
SwitchAgent,
AgentActivity,
}
3 changes: 3 additions & 0 deletions frontend/hooks/useFeatureFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const FeatureFlagsSchema = z.enum([
'manage-wallet',
'rewards-streak',
'staking-contract-section',
'agent-activity',
]);
type FeatureFlags = z.infer<typeof FeatureFlagsSchema>;

Expand All @@ -29,12 +30,14 @@ const FEATURES_CONFIG = FeaturesConfigSchema.parse({
'last-transactions': true,
'rewards-streak': true,
'staking-contract-section': true,
'agent-activity': false,
},
[AgentType.Memeooorr]: {
'manage-wallet': false,
'last-transactions': false,
'rewards-streak': false,
'staking-contract-section': false,
'agent-activity': true,
},
});

Expand Down
3 changes: 3 additions & 0 deletions frontend/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useMemo } from 'react';

import { AgentActivityPage } from '@/components/AgentActivity';
import { AgentSelection } from '@/components/AgentSelection';
import { Main } from '@/components/MainPage';
import { ManageStakingPage } from '@/components/ManageStakingPage';
Expand Down Expand Up @@ -61,6 +62,8 @@ export default function Home() {
return <RewardsHistory />;
case Pages.AddBackupWalletViaSafe:
return <AddBackupWalletViaSafePage />;
case Pages.AgentActivity:
return <AgentActivityPage />;
default:
return <Main />;
}
Expand Down
17 changes: 17 additions & 0 deletions frontend/types/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,20 @@ export type AgentConfig = {
displayName: string;
description: string;
};

export type AgentHealthCheck = {
seconds_since_last_transition: number;
is_tm_healthy: boolean;
period: number;
reset_pause_duration: number;
is_transitioning_fast: boolean;
rounds: string[];
rounds_info?: Record<
string,
{
name: string;
description: string;
transitions: Record<string, string>;
}
>;
};

0 comments on commit 80d51b9

Please sign in to comment.