Skip to content

Commit d8a7475

Browse files
feat : Header Perfomance Indicator
1 parent 790e618 commit d8a7475

File tree

7 files changed

+139
-46
lines changed

7 files changed

+139
-46
lines changed

client/assets/icons/coin/btc.svg

Lines changed: 15 additions & 0 deletions
Loading

client/components/CryptoPriceBar.vue

Lines changed: 97 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,67 @@
55
<div class="text-secondary text-xs">Loading data...</div>
66
</div>
77
<div v-else class="flex items-center justify-between">
8+
89
<!-- Crypto Prices -->
9-
<div class="flex items-center space-x-8">
10-
<div v-if="cryptoPrices.length === 0" class="text-secondary text-xs">
11-
No price data available
12-
</div>
13-
<template v-else>
14-
<div
15-
v-for="crypto in cryptoPrices"
16-
:key="crypto.symbol"
17-
class="flex items-center space-x-2"
18-
>
10+
<div class="flex items-center space-x-4">
11+
<!-- <template v-for="(crypto, index) in displayCryptoPrices" :key="crypto.symbol">
12+
<div class="flex items-center space-x-2">
13+
<img
14+
v-if="crypto.icon"
15+
:src="crypto.icon"
16+
:alt="crypto.symbol"
17+
class="w-8 h-8 object-contain rounded-full border border-gray-300 p-0.5"
18+
@error="handleImageError"
19+
/>
1920
<span class="text-xs font-bold text-primary uppercase tracking-wider">{{ crypto.symbol }}</span>
2021
<span class="text-xs text-secondary">{{ formatPrice(crypto.price) }}</span>
2122
</div>
22-
</template>
23+
<div v-if="index < displayCryptoPrices.length - 1" class="h-4 w-px bg-gray-300"></div>
24+
</template> -->
2325
</div>
2426

2527
<!-- Performance Summary -->
2628
<div class="flex items-center space-x-6">
27-
<div class="flex items-center space-x-2">
28-
<span class="text-xs text-secondary uppercase tracking-wider">Highest:</span>
29-
<span class="text-xs font-bold text-primary">{{ highest.model }}</span>
30-
<span class="text-xs text-secondary">{{ formatPrice(highest.value) }}</span>
31-
<span class="text-xs font-bold text-primary">+{{ highest.change }}%</span>
29+
<div class="flex items-center space-x-3">
30+
<!-- <span class="text-xs text-secondary uppercase tracking-wider">Highest:</span> -->
31+
<img
32+
v-if="highestIcon"
33+
:src="highestIcon"
34+
:alt="highest.model"
35+
class="w-8 h-8 object-contain rounded-full border border-gray-300 p-0.5"
36+
@error="handleImageError"
37+
/>
38+
<span class="text-xs text-mono-text-secondary font-bold">{{ highest.model }}</span>
39+
<span class="text-xs font-bold text-primary">{{ formatPrice(highest.value) }}</span>
40+
<span :class="[
41+
'text-xs font-bold',
42+
highest.change >= 0 ? 'text-green-600' : 'text-red-600'
43+
]">
44+
{{ formatChange(highest.change) }}
45+
</span>
3246
</div>
47+
48+
<!-- <div class="h-4 w-px bg-gray-300"></div>
49+
3350
<div class="flex items-center space-x-2">
3451
<span class="text-xs text-secondary uppercase tracking-wider">Lowest:</span>
52+
<img
53+
v-if="lowestIcon"
54+
:src="lowestIcon"
55+
:alt="lowest.model"
56+
class="w-8 h-8 object-contain rounded-full border border-gray-300 p-0.5"
57+
@error="handleImageError"
58+
/>
3559
<span class="text-xs font-bold text-primary">{{ lowest.model }}</span>
3660
<span class="text-xs text-secondary">{{ formatPrice(lowest.value) }}</span>
37-
<span class="text-xs font-bold text-primary">{{ lowest.change }}%</span>
38-
</div>
61+
<span :class="[
62+
'text-xs font-bold',
63+
lowest.change >= 0 ? 'text-green-600' : 'text-red-600'
64+
]">
65+
{{ formatChange(lowest.change) }}
66+
</span>
67+
</div> -->
68+
3969
</div>
4070
</div>
4171
</div>
@@ -45,22 +75,71 @@
4575
<script setup lang="ts">
4676
import type { CryptoPrice, ModelPerformance } from '~/types'
4777
import { formatNumber } from '~/composables/useNumberFormat'
78+
import { getModelIcon, getCoinIcon } from '~/config/assets'
4879
4980
interface Props {
5081
cryptoPrices: CryptoPrice[]
5182
performance: ModelPerformance
5283
loading?: boolean
5384
}
5485
86+
interface CryptoPriceWithIcon extends CryptoPrice {
87+
icon: string
88+
}
89+
5590
const props = defineProps<Props>()
5691
5792
const highest = computed(() => props.performance.highest)
5893
const lowest = computed(() => props.performance.lowest)
5994
95+
// Resolve icon paths from coin symbols returned by API
96+
const highestIcon = computed(() => {
97+
if (!highest.value?.icon) return ''
98+
// API returns coin symbol, resolve to icon path
99+
return getModelIcon(highest.value.icon)
100+
})
101+
102+
const lowestIcon = computed(() => {
103+
if (!lowest.value?.icon) return ''
104+
// API returns coin symbol, resolve to icon path
105+
return getModelIcon(lowest.value.icon)
106+
})
107+
108+
// Dummy crypto prices for display when no data is available
109+
const dummyCryptoPrices: CryptoPriceWithIcon[] = [
110+
{ symbol: 'BTC', name: 'Bitcoin', price: 104577.50, icon: getCoinIcon('BTC') },
111+
{ symbol: 'ETH', name: 'Ethereum', price: 3505.75, icon: getCoinIcon('ETH') },
112+
{ symbol: 'SOL', name: 'Solana', price: 158.56, icon: getCoinIcon('SOL') },
113+
{ symbol: 'BNB', name: 'Binance Coin', price: 952.14, icon: getCoinIcon('BNB') },
114+
{ symbol: 'DOGE', name: 'Dogecoin', price: 0.1642, icon: getCoinIcon('DOGE') },
115+
{ symbol: 'XRP', name: 'XRP', price: 2.27, icon: getCoinIcon('XRP') },
116+
]
117+
118+
// Display crypto prices with icons - use dummy data if no real data
119+
const displayCryptoPrices = computed<CryptoPriceWithIcon[]>(() => {
120+
if (props.cryptoPrices.length > 0) {
121+
return props.cryptoPrices.map(crypto => ({
122+
...crypto,
123+
icon: getModelIcon(crypto.symbol)
124+
}))
125+
}
126+
return dummyCryptoPrices
127+
})
128+
60129
const formatPrice = (price: number): string => {
61130
const formatted = formatNumber(price)
62131
return `$${formatted}`
63132
}
133+
134+
const formatChange = (change: number): string => {
135+
const sign = change >= 0 ? '+' : ''
136+
return `${sign}${change.toFixed(2)}%`
137+
}
138+
139+
const handleImageError = (event: Event) => {
140+
const img = event.target as HTMLImageElement
141+
img.style.display = 'none'
142+
}
64143
</script>
65144

66145
<style scoped>

client/components/MainChart.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ const chartOptions = computed(() => {
170170
// max: yAxisMax,
171171
// tickAmount: Math.ceil((yAxisMax - yAxisMin) / 5000),
172172
min: 7000,
173-
max: 12000,
174-
tickAmount: 5,
173+
max: 13000,
174+
tickAmount: 6,
175175
labels: {
176176
style: {
177177
colors: '#000000',

client/config/assets.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
// Import coin icons
7+
import btcIcon from '~/assets/icons/coin/btc.svg'
78
import ethIcon from '~/assets/icons/coin/eth.svg'
89
import solIcon from '~/assets/icons/coin/sol.svg'
910
import bnbIcon from '~/assets/icons/coin/bnb.svg'
@@ -40,7 +41,7 @@ export const COINS: Record<string, CoinConfig> = {
4041
BTC: {
4142
symbol: 'BTC',
4243
fullName: 'Bitcoin',
43-
icon: '', // No BTC icon available yet
44+
icon: btcIcon,
4445
},
4546
ETH: {
4647
symbol: 'ETH',

client/server/api/performance.get.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import { getDb, accounts } from '~/server/utils/db'
2-
import { desc, asc, sql } from 'drizzle-orm'
2+
3+
// Model name mapping - same as in accounts.get.ts
4+
// In the future, this can be replaced with a model_name field in the accounts table
5+
function getModelName(accountId: string): string {
6+
// For now, since there's only one account, return the known model name
7+
// This can be enhanced to check agent_invocations or use a mapping table
8+
// TODO: Add model_name field to accounts table or create a mapping table
9+
return 'google/gemini-2.0-flash-001'
10+
}
311

412
export default defineEventHandler(async (event) => {
513
try {
614
const db = getDb()
715

8-
// Get all accounts ordered by current balance (performance)
16+
// Get all accounts
917
const allAccounts = await db
1018
.select()
1119
.from(accounts)
12-
.orderBy(desc(accounts.current_balance))
1320

1421
if (allAccounts.length === 0) {
1522
return {
@@ -18,55 +25,46 @@ export default defineEventHandler(async (event) => {
1825
}
1926
}
2027

21-
// Use stored account_value and total_return_percent instead of calculating
22-
const accountsWithChange = allAccounts.map(account => {
28+
// Process accounts: use stored values, calculate if missing
29+
const accountsWithMetrics = allAccounts.map(account => {
2330
const initial = parseFloat(account.initial_balance)
2431
// Use stored account_value if available, otherwise use current_balance
2532
const accountValue = account.account_value
2633
? parseFloat(account.account_value)
2734
: parseFloat(account.current_balance)
2835
// Use stored total_return_percent if available, otherwise calculate
29-
const change = account.total_return_percent
36+
const change = account.total_return_percent !== null && account.total_return_percent !== undefined
3037
? parseFloat(account.total_return_percent)
3138
: (initial > 0 ? ((accountValue - initial) / initial) * 100 : 0)
3239

3340
return {
3441
id: account.id,
3542
account_value: accountValue,
36-
current_balance: parseFloat(account.current_balance),
37-
initial_balance: initial,
3843
change,
3944
}
4045
})
4146

42-
// Sort by account_value (total value) instead of just current_balance
43-
accountsWithChange.sort((a, b) => b.account_value - a.account_value)
47+
// Sort by account_value (total value) - highest first
48+
accountsWithMetrics.sort((a, b) => b.account_value - a.account_value)
4449

4550
// Get highest (first in descending order)
46-
const highest = accountsWithChange[0]
51+
const highest = accountsWithMetrics[0]
4752

4853
// Get lowest (last in array)
49-
const lowest = accountsWithChange[accountsWithChange.length - 1]
50-
51-
// For now, we'll use account IDs as model names
52-
// You might want to add a model_name field to accounts table later
53-
const modelNameMap: Record<string, string> = {
54-
// This can be enhanced to map account IDs to model names
55-
// For now, we'll use generic names
56-
}
54+
const lowest = accountsWithMetrics[accountsWithMetrics.length - 1]
5755

5856
return {
5957
highest: {
60-
model: modelNameMap[highest.id] || `Account ${highest.id.slice(0, 8)}`,
61-
value: highest.account_value, // Use total account value instead of just balance
58+
model: getModelName(highest.id).toUpperCase(),
59+
value: highest.account_value,
6260
change: highest.change,
63-
icon: 'purple', // Default icon
61+
icon: 'gemini'
6462
},
6563
lowest: {
66-
model: modelNameMap[lowest.id] || `Account ${lowest.id.slice(0, 8)}`,
67-
value: lowest.account_value, // Use total account value instead of just balance
64+
model: getModelName(lowest.id).toUpperCase(),
65+
value: lowest.account_value,
6866
change: lowest.change,
69-
icon: 'green', // Default icon
67+
icon: 'gemini'
7068
},
7169
}
7270
} catch (error) {
2.45 MB
Loading

0 commit comments

Comments
 (0)