diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 06548de..a227555 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,5 +1,7 @@ 'use client'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useAppDispatch, useAppSelector } from '@/store'; @@ -10,15 +12,15 @@ import { Card } from '@/components/ui/Card'; import { ChatLayout } from '@/components/layout/ChatLayout'; import TransactionTable from '@/components/dashboard/TransactionTable'; import { - Copy, - ExternalLink, - Clock, - ArrowUpRight, - ArrowDownLeft, - RefreshCw, + AlertTriangle, + Bot, CheckCircle2, - Circle, + Clipboard, + FlaskConical, Loader2, + RefreshCw, + Sparkles, + Wand2 ShieldCheck, Wallet, Coins, @@ -27,8 +29,34 @@ import { AlertTriangle, Droplets } from 'lucide-react'; -import { formatAddress, formatTokenAmount } from '@/utils/format'; import toast from 'react-hot-toast'; +import { ChatLayout } from '@/components/layout/ChatLayout'; +import { Button } from '@/components/ui/Button'; +import { Card } from '@/components/ui/Card'; +import apiService from '@/services/api'; +import { useAppSelector } from '@/store'; +import { PromptVersion, PromptVersionRecord } from '@/types'; + +const DEFAULT_TEST_INPUT = 'Help a user move idle USDC into the highest-yield low-risk strategy.'; +const DEFAULT_VARIABLES = JSON.stringify( + { + agentName: 'ChenPilot', + userName: 'Ada', + network: 'mainnet', + riskLevel: 'low', + tone: 'clear and confident' + }, + null, + 2 +); + +function isRecord(value: unknown): value is PromptVersionRecord { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function getString(value: unknown): string | null { + return typeof value === 'string' && value.trim().length > 0 ? value : null; +} import LiquidityPoolStats from '@/components/widgets/LiquidityPoolStats'; export default function DashboardPage() { @@ -42,45 +70,262 @@ export default function DashboardPage() { const [pageSize, setPageSize] = useState(10); const [totalCount, setTotalCount] = useState(0); - useEffect(() => { - if (!isAuthenticated) { - router.push('/auth/login'); - return; +function getBoolean(value: unknown): boolean | null { + return typeof value === 'boolean' ? value : null; +} + +function getArray(value: unknown): unknown[] { + return Array.isArray(value) ? value : []; +} + +function extractVersionRecords(payload: unknown): PromptVersionRecord[] { + if (Array.isArray(payload)) { + return payload.filter(isRecord); + } + + if (!isRecord(payload)) { + return []; + } + + const directCollections = [ + payload.versions, + payload.data, + payload.items, + payload.results + ]; + + for (const candidate of directCollections) { + if (Array.isArray(candidate)) { + return candidate.filter(isRecord); } + } - // Load initial user data and account status - dispatch(loadUser()); - dispatch(getAccountStatus()); - dispatch(getBalance()); + if (isRecord(payload.data)) { + const nestedCollections = [ + payload.data.versions, + payload.data.items, + payload.data.results + ]; - // Set up polling for account status and balance if not fully deployed - const pollInterval = setInterval(() => { - if (!status?.isDeployed || !status?.isFunded) { - dispatch(getAccountStatus()); - dispatch(getBalance()); + for (const candidate of nestedCollections) { + if (Array.isArray(candidate)) { + return candidate.filter(isRecord); + } + } + } + + return []; +} + +function getTemplate(record: PromptVersionRecord): string { + return ( + getString(record.template) ?? + getString(record.prompt) ?? + getString(record.systemPrompt) ?? + getString(record.content) ?? + getString(record.body) ?? + getString(record.text) ?? + '' + ); +} + +function getLabel(record: PromptVersionRecord, fallbackIndex: number): string { + return ( + getString(record.name) ?? + getString(record.title) ?? + getString(record.label) ?? + getString(record.version) ?? + `Template ${fallbackIndex + 1}` + ); +} + +function normalizeVersion(record: PromptVersionRecord, index: number): PromptVersion { + const id = + getString(record.id) ?? + getString(record._id) ?? + getString(record.uuid) ?? + `version-${index + 1}`; + + const rawTags = getArray(record.tags); + const tags = rawTags + .map((tag) => (typeof tag === 'string' ? tag.trim() : '')) + .filter(Boolean); + + const activeFlag = + getBoolean(record.isActive) ?? + getBoolean(record.active) ?? + getBoolean(record.is_active) ?? + (getString(record.status)?.toLowerCase() === 'active'); + + return { + id, + label: getLabel(record, index), + description: + getString(record.description) ?? + getString(record.summary) ?? + getString(record.notes) ?? + 'No description provided for this prompt version.', + template: getTemplate(record), + version: getString(record.version) ?? id, + isActive: Boolean(activeFlag), + createdAt: + getString(record.createdAt) ?? + getString(record.created_at) ?? + getString(record.insertedAt) ?? + null, + updatedAt: + getString(record.updatedAt) ?? + getString(record.updated_at) ?? + getString(record.modifiedAt) ?? + null, + tags, + raw: record + }; +} + +function safelyFormatDate(value: string | null): string { + if (!value) { + return 'Unknown'; + } + + const parsed = new Date(value); + return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString(); +} + +function parseVariables(rawValue: string): { parsed: Record; error: string | null } { + try { + const value = JSON.parse(rawValue) as unknown; + + if (!isRecord(value)) { + return { parsed: {}, error: 'Variables must be a JSON object.' }; + } + + const flattened = Object.entries(value).reduce>((accumulator, [key, entry]) => { + if (entry === null || entry === undefined) { + accumulator[key] = ''; + } else if (typeof entry === 'string') { + accumulator[key] = entry; } else { - dispatch(getBalance()); + accumulator[key] = JSON.stringify(entry); } - }, 10000); - return () => clearInterval(pollInterval); - }, [dispatch, isAuthenticated, router, status?.isDeployed, status?.isFunded]); + return accumulator; + }, {}); + + return { parsed: flattened, error: null }; + } catch { + return { parsed: {}, error: 'Variables JSON is invalid.' }; + } +} + +function renderTemplate(template: string, variables: Record): string { + return template + .replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key: string) => variables[key] ?? match) + .replace(/\$\{\s*([a-zA-Z0-9_.-]+)\s*\}/g, (match, key: string) => variables[key] ?? match); +} + +function getPlaceholders(template: string): string[] { + const placeholderMatches = template.match(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}|\$\{\s*([a-zA-Z0-9_.-]+)\s*\}/g) ?? []; + const normalized = placeholderMatches.map((placeholder) => + placeholder.replace(/^\{\{\s*|\s*\}\}$|^\$\{\s*|\s*\}$/g, '') + ); + + return Array.from(new Set(normalized)); +} + +function copyText(text: string, label: string) { + navigator.clipboard.writeText(text); + toast.success(`${label} copied`); +} + +export default function DashboardPage() { + const router = useRouter(); + const { isAuthenticated, user } = useAppSelector((state) => state.auth); + const [versions, setVersions] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + const [isActivating, setIsActivating] = useState(false); + const [error, setError] = useState(null); + const [testInput, setTestInput] = useState(DEFAULT_TEST_INPUT); + const [variablesText, setVariablesText] = useState(DEFAULT_VARIABLES); + + const loadVersions = async (showRefreshState = false) => { + if (showRefreshState) { + setIsRefreshing(true); + } else { + setIsLoading(true); + } + + try { + setError(null); + const response = await apiService.getPromptVersions(); + const normalizedVersions = extractVersionRecords(response).map(normalizeVersion); + + setVersions(normalizedVersions); + setSelectedId((currentSelectedId) => { + if (currentSelectedId && normalizedVersions.some((version) => version.id === currentSelectedId)) { + return currentSelectedId; + } + + return normalizedVersions.find((version) => version.isActive)?.id ?? normalizedVersions[0]?.id ?? null; + }); + } catch (requestError: unknown) { + const message = + requestError instanceof Error ? requestError.message : 'Failed to load prompt versions.'; + setError(message); + toast.error(message); + } finally { + setIsLoading(false); + setIsRefreshing(false); + } + }; useEffect(() => { if (!isAuthenticated) { + router.push('/auth/login'); return; } - const refreshNetworkStatus = () => { - dispatch(getStellarNetworkStatus()); + void loadVersions(); + }, [isAuthenticated, router]); + + const selectedVersion = useMemo( + () => versions.find((version) => version.id === selectedId) ?? versions[0] ?? null, + [selectedId, versions] + ); + + const activeVersion = useMemo( + () => versions.find((version) => version.isActive) ?? null, + [versions] + ); + + const parsedVariables = useMemo(() => { + const { parsed, error: parseError } = parseVariables(variablesText); + + return { + variables: { + ...parsed, + input: testInput, + userInput: testInput, + user_query: testInput + }, + error: parseError }; + }, [testInput, variablesText]); - refreshNetworkStatus(); - const interval = setInterval(refreshNetworkStatus, 15000); + const renderedPrompt = useMemo(() => { + if (!selectedVersion) { + return ''; + } - return () => clearInterval(interval); - }, [dispatch, isAuthenticated]); + return renderTemplate(selectedVersion.template, parsedVariables.variables); + }, [parsedVariables.variables, selectedVersion]); + const unresolvedPlaceholders = useMemo(() => { + if (!selectedVersion) { + return []; + } // Fetch transactions useEffect(() => { if (!isAuthenticated || !user?.id) { @@ -113,66 +358,42 @@ export default function DashboardPage() { toast.success(`${label} copied to clipboard`); }; + return getPlaceholders(selectedVersion.template).filter( + (placeholder) => parsedVariables.variables[placeholder] === undefined + ); + }, [parsedVariables.variables, selectedVersion]); - const quickActions = [ - { - title: 'Chat with AI Agent', - description: 'Ask questions or execute DeFi operations', - action: () => router.push('/chat'), - color: 'bg-blue-500', - }, - { - title: 'Manage Contacts', - description: 'Add and organize your contacts', - action: () => router.push('/contacts'), - color: 'bg-green-500', - }, - { - title: 'View Transactions', - description: 'Check your transaction history', - action: () => router.push('/transactions'), - color: 'bg-purple-500', - }, - ]; + const handleActivate = async () => { + if (!selectedVersion || selectedVersion.isActive) { + return; + } - const networkStatusLabel = - network.status === 'healthy' - ? 'Healthy' - : network.status === 'degraded' - ? 'Degraded' - : network.status === 'down' - ? 'Down' - : 'Checking'; - - const networkStatusColor = - network.status === 'healthy' - ? 'text-green-400' - : network.status === 'degraded' - ? 'text-yellow-400' - : network.status === 'down' - ? 'text-red-400' - : 'text-gray-300'; - - const syncStatusLabel = - network.accountSyncState === 'synced' - ? 'Synced' - : network.accountSyncState === 'syncing' - ? 'Syncing' - : 'Desynced'; - - const syncStatusColor = - network.accountSyncState === 'synced' - ? 'text-green-400' - : network.accountSyncState === 'syncing' - ? 'text-yellow-400' - : 'text-red-400'; + setIsActivating(true); + + try { + await apiService.activatePromptVersion(selectedVersion.id); + setVersions((currentVersions) => + currentVersions.map((version) => ({ + ...version, + isActive: version.id === selectedVersion.id + })) + ); + toast.success(`${selectedVersion.label} is now active`); + } catch (requestError: unknown) { + const message = + requestError instanceof Error ? requestError.message : 'Failed to activate the selected version.'; + toast.error(message); + } finally { + setIsActivating(false); + } + }; if (!isAuthenticated) { return ( -
+
-
-

Loading...

+ +

Redirecting to login...

); @@ -180,108 +401,27 @@ export default function DashboardPage() { return ( -
- {/* Simple Background */} -
- {/* Grid Pattern */} -
-
- -
- {/* Welcome Section */} -
-

- Welcome back, {user?.name || 'User'}! -

-

- Here's an overview of your ChenPilot account and recent activity. -

-
- - {/* Account Lifecycle Section */} -
-
-

- - Account Lifecycle -

-
-
- - {status?.isDeployed ? 'Network Live' : 'Pending Activation'} - -
-
- -
- {/* Step 1: Created */} - -
-
-
-
- -
-

1. Created

-
-

Account identity established on-chain.

- {status?.address ? ( -
-
- {formatAddress(status.address)} - -
-
- Active -
-
- ) : ( -
- Initializing... -
- )} -
+
+
+
+ +
+
+
+
+
+ + Prompt Version Control
- - - {/* Step 2: Funded */} - -
-
-
-
- -
-

2. Funded

-
-

Resources added for network operations.

- {status?.isFunded ? ( -
-
- {balance ? formatTokenAmount(balance.balance, 4, 'STRK') : '0.00 STRK'} -
-
- Verified -
-
- ) : status?.address ? ( -
- Detecting funds... -
- ) : ( -
- Waiting for account -
- )} -
-
-
+

+ Agent prompt management dashboard +

+

+ Review every available prompt version, simulate template output with sample variables, and promote the best version without leaving the admin workspace. +

+
+
{/* Step 3: Deployed */}
@@ -397,126 +537,301 @@ export default function DashboardPage() {
-
-
-
- Latest Ledger - - {network.latestLedger ?? 'Loading...'} - +
+
Signed in as
+
{user?.email ?? 'Administrator'}
-
- Ledger Age - - {network.ledgerAgeSeconds !== null - ? `${network.ledgerAgeSeconds}s` - : 'Loading...'} - +
+
+
+ +
+ +
+
+

Available versions

+

{versions.length}

-
- Last Updated - - {network.lastUpdated - ? new Date(network.lastUpdated).toLocaleTimeString() - : 'Loading...'} - + +
+ + +
+
+

Active version

+

+ {activeVersion?.label ?? 'No active version'} +

+
- {network.congestion && ( -
- - - Congestion detected. Transactions may take longer to confirm. - + + +
+
+

Simulation status

+

+ {parsedVariables.error + ? 'Fix variables JSON' + : unresolvedPlaceholders.length > 0 + ? `${unresolvedPlaceholders.length} placeholder${unresolvedPlaceholders.length === 1 ? '' : 's'} missing` + : 'Ready to test'} +

- )} + +
+
- -
+ {error && ( +
+
+
-

- Account Sync State -

-
- - {syncStatusLabel} - -
-

- Based on the latest ledger signal from Horizon. -

+

The versions API could not be loaded.

+

{error}

- {network.accountSyncState === 'desynced' && ( -
- - - Account appears out of sync. Try refreshing or check network conditions. - +
+ )} + +
+ + {isLoading ? ( +
+
+ +

Fetching prompt versions...

+
- )} - {network.accountSyncState === 'syncing' && ( -
- - - Syncing with the network. Recent updates may be delayed. - + ) : versions.length === 0 ? ( +
+ No prompt versions were returned by the API. +
+ ) : ( +
+ {versions.map((version) => { + const isSelected = version.id === selectedVersion?.id; + + return ( + + ); + })}
)} -
-
- {/* Quick Actions */} -
-

- Quick Actions -

-
- {quickActions.map((action, index) => ( - -
-
-

- {action.title} -

+
+ router.push('/chat')} + onClick={() => void handleActivate()} + disabled={selectedVersion.isActive} + loading={isActivating} + className={`${ + selectedVersion.isActive + ? 'bg-emerald-500/20 text-emerald-200 hover:bg-emerald-500/20' + : 'bg-cyan-500 text-slate-950 hover:bg-cyan-400' + }`} > - View All + {selectedVersion.isActive ? 'Currently Active' : 'Activate Version'} + ) : undefined + } + > + {selectedVersion ? ( +
+
+
+

Created

+

{safelyFormatDate(selectedVersion.createdAt)}

+
+
+

Updated

+

{safelyFormatDate(selectedVersion.updatedAt)}

+
+
+

Source endpoint

+

PATCH /versions/{selectedVersion.id}/activate

+
+
+ +
+
+
+

Description

+

{selectedVersion.description}

+
+ +
+
+ +
+
+
+

Template source

+

Original prompt body from the selected version.

+
+ {selectedVersion.template.length} chars +
+
+                        {selectedVersion.template || 'No template body found on this version.'}
+                      
+
-
- {messages.slice(-5).reverse().map((message, index) => ( -
-
-
-
- - {message.type === 'user' ? 'You' : 'AI Agent'} - + ) : ( +
+ Select a prompt version to inspect details and activate it. +
+ )} + + + + {selectedVersion ? ( +
+
+
+ +