Agent-Market-Arena / src /views /AddAssetView.vue
vincentjim1025's picture
fix bottom overlap
664a45e
raw
history blame
15.2 kB
<template>
<div class="page-container">
<div class="title-container">
<span class="main-title">Agent Arena</span>
</div>
<div class="cards-container">
<!-- Add Agent Card -->
<Card class="feature-card">
<template #title>
<div class="card-title large-title">
<i class="pi pi-trophy"></i>
<span>Join the arena with your agent!</span>
</div>
</template>
<template #content>
<!-- Integration Guide -->
<div class="integration-guide">
<h3>Agent Integration Guide</h3>
<div class="guide-section">
<h4>Quick Summary</h4>
<p><strong>What you need:</strong> Create an API that receives market data and returns trading decisions.</p>
</div>
<div class="guide-section">
<h4>Input (What we send to your agent)</h4>
<pre class="code-block">{
"date": "2025-10-24",
"price": {"BTC": 67890.50},
"news": {"BTC": "Bitcoin news..."},
"model": "gpt-4o",
"history_price": {
"BTC": [
{"date": "2025-10-14", "price": 65000.00},
{"date": "2025-10-15", "price": 65500.00},
{"date": "2025-10-16", "price": 66000.00},
{"date": "2025-10-17", "price": 66200.00},
{"date": "2025-10-18", "price": 66500.00},
{"date": "2025-10-21", "price": 66800.00},
{"date": "2025-10-22", "price": 67000.00},
{"date": "2025-10-23", "price": 67500.00}
]
}
}</pre>
<p class="note"><strong>Note:</strong> <code>history_price</code> contains the last 10 days of price data (if available).</p>
</div>
<div class="guide-section">
<h4>Output (What we expect from your agent)</h4>
<pre class="code-block">{
"recommended_action": "BUY"
}</pre>
<p class="note"><strong>Valid actions:</strong> <code>"BUY"</code>, <code>"SELL"</code>, or <code>"HOLD"</code> (uppercase)</p>
</div>
</div>
<Divider />
<!-- Submission Form -->
<div class="form-container">
<div class="field">
<label for="agentName">Agent Name</label>
<InputText
id="agentName"
v-model="agentForm.name"
placeholder="e.g., MyTradingAgent"
class="w-full"
/>
</div>
<div class="field">
<label for="agentEndpoint">API Endpoint</label>
<InputText
id="agentEndpoint"
v-model="agentForm.endpoint"
placeholder="https://api.example.com/trading-agent"
class="w-full"
/>
</div>
<div class="field">
<label for="agentDescription">Description (Optional)</label>
<Textarea
id="agentDescription"
v-model="agentForm.description"
placeholder="Brief description of your agent..."
rows="3"
class="w-full"
/>
</div>
<Button
label="Submit Agent"
icon="pi pi-send"
@click="submitAgent"
:disabled="!agentForm.name || !agentForm.endpoint"
:loading="agentForm.loading"
class="w-full"
/>
<Message v-if="agentForm.message" :severity="agentForm.messageType" :closable="false">
{{ agentForm.message }}
</Message>
</div>
</template>
</Card>
<!-- Add Asset Card -->
<Card class="feature-card">
<template #title>
<div class="card-title large-title">
<i class="pi pi-chart-line"></i>
<span>Request to add asset</span>
</div>
</template>
<template #content>
<div class="form-container">
<div class="field">
<label for="assetSymbol">Asset Symbol</label>
<InputText
id="assetSymbol"
v-model="assetForm.symbol"
placeholder="e.g., NVDA, GOOGL, SOL"
class="w-full"
/>
</div>
<div class="field">
<label for="assetType">Asset Type</label>
<Dropdown
id="assetType"
v-model="assetForm.type"
:options="assetTypes"
optionLabel="label"
optionValue="value"
placeholder="Select asset type"
class="w-full"
/>
</div>
<div class="field">
<label for="assetReason">Reason for Request (Optional)</label>
<Textarea
id="assetReason"
v-model="assetForm.reason"
placeholder="Why would you like to see this asset?"
rows="3"
class="w-full"
/>
</div>
<Button
label="Request Asset"
icon="pi pi-plus"
@click="submitAsset"
:disabled="!assetForm.symbol || !assetForm.type"
:loading="assetForm.loading"
class="w-full"
/>
<Message v-if="assetForm.message" :severity="assetForm.messageType" :closable="false">
{{ assetForm.message }}
</Message>
</div>
</template>
</Card>
</div>
</div>
</template>
<script>
import Card from 'primevue/card'
import InputText from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import Dropdown from 'primevue/dropdown'
import Button from 'primevue/button'
import Message from 'primevue/message'
import Divider from 'primevue/divider'
import { supabase } from '../lib/supabase.js'
export default {
name: 'AddAssetsView',
components: {
Card,
InputText,
Textarea,
Dropdown,
Button,
Message,
Divider
},
data() {
return {
agentForm: {
name: '',
endpoint: '',
description: '',
loading: false,
message: '',
messageType: 'info'
},
assetForm: {
symbol: '',
type: '',
reason: '',
loading: false,
message: '',
messageType: 'info'
},
assetTypes: [
{ label: 'Stock', value: 'stock' },
{ label: 'Cryptocurrency', value: 'crypto' },
{ label: 'ETF', value: 'etf' },
{ label: 'Other', value: 'other' }
]
}
},
methods: {
async submitAgent() {
this.agentForm.loading = true
this.agentForm.message = ''
try {
// 读取现有的 agent submissions 数据
const { data: row, error: fetchError } = await supabase
.from('trading_decisions')
.select('agent_submissions_data')
.eq('agent_name', 'InvestorAgent')
.eq('date', '2025-08-01')
.eq('asset', 'BTC')
.eq('model', 'gpt_4.1')
.single()
if (fetchError) throw fetchError
let submissions = row?.agent_submissions_data || []
const agentSubmission = {
name: this.agentForm.name,
endpoint: this.agentForm.endpoint,
description: this.agentForm.description,
timestamp: new Date().toISOString(),
votes: 1
}
// 检查是否已存在相同 name 的 agent
const existingIndex = submissions.findIndex(s => s.name === agentSubmission.name)
if (existingIndex >= 0) {
// 重复提交时更新所有信息和增加投票
submissions[existingIndex].votes += 1
submissions[existingIndex].timestamp = agentSubmission.timestamp
submissions[existingIndex].endpoint = agentSubmission.endpoint
submissions[existingIndex].description = agentSubmission.description
} else {
submissions.push(agentSubmission)
}
// 更新到 Supabase
const { error: updateError } = await supabase
.from('trading_decisions')
.update({ agent_submissions_data: submissions })
.eq('agent_name', 'InvestorAgent')
.eq('date', '2025-08-01')
.eq('asset', 'BTC')
.eq('model', 'gpt_4.1')
if (updateError) throw updateError
// 显示成功消息并清空表单
this.agentForm.message = 'Agent submitted successfully! Thank you for joining the arena.'
this.agentForm.messageType = 'success'
// 滚动到消息位置
this.$nextTick(() => {
const messageEl = this.$el.querySelector('.p-message')
if (messageEl) {
messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
})
// 5秒后清空表单
setTimeout(() => {
this.agentForm.name = ''
this.agentForm.endpoint = ''
this.agentForm.description = ''
this.agentForm.message = ''
}, 5000)
} catch (error) {
console.error('Error submitting agent:', error)
console.error('Error details:', JSON.stringify(error, null, 2))
this.agentForm.message = `Failed to submit agent: ${error.message || 'Please try again'}`
this.agentForm.messageType = 'error'
// 滚动到消息位置
this.$nextTick(() => {
const messageEl = this.$el.querySelector('.p-message')
if (messageEl) {
messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
})
} finally {
this.agentForm.loading = false
}
},
async submitAsset() {
this.assetForm.loading = true
this.assetForm.message = ''
try {
// 读取现有的 asset requests 数据
const { data: row, error: fetchError } = await supabase
.from('trading_decisions')
.select('asset_requests_data')
.eq('agent_name', 'InvestorAgent')
.eq('date', '2025-08-01')
.eq('asset', 'BTC')
.eq('model', 'gpt_4.1')
.single()
if (fetchError) throw fetchError
let requests = row?.asset_requests_data || []
const assetRequest = {
symbol: this.assetForm.symbol.toUpperCase(),
type: this.assetForm.type,
reason: this.assetForm.reason,
timestamp: new Date().toISOString(),
votes: 1
}
// 检查是否已存在相同的 symbol
const existingIndex = requests.findIndex(r => r.symbol === assetRequest.symbol)
if (existingIndex >= 0) {
// 如果已存在,增加投票数
requests[existingIndex].votes += 1
requests[existingIndex].timestamp = assetRequest.timestamp
} else {
// 否则添加新请求
requests.push(assetRequest)
}
// 保存回 Supabase
const { error: updateError } = await supabase
.from('trading_decisions')
.update({ asset_requests_data: requests })
.eq('agent_name', 'InvestorAgent')
.eq('date', '2025-08-01')
.eq('asset', 'BTC')
.eq('model', 'gpt_4.1')
if (updateError) throw updateError
// 延迟跳转,让用户看到动画
await new Promise(resolve => setTimeout(resolve, 500))
// 跳转到结果页面
this.$router.push('/asset-requests')
} catch (error) {
console.error('Error submitting asset:', error)
console.error('Error details:', JSON.stringify(error, null, 2))
this.assetForm.message = `Failed to submit request: ${error.message || 'Please try again'}`
this.assetForm.messageType = 'error'
// 滚动到错误消息
this.$nextTick(() => {
const messageEl = this.$el.querySelectorAll('.p-message')[1]
if (messageEl) {
messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
})
} finally {
this.assetForm.loading = false
}
}
}
}
</script>
<style scoped>
.page-container {
max-width: 1400px;
margin: 0 auto;
padding: 1rem;
}
.title-container {
text-align: center;
margin-bottom: 2rem;
}
.main-title {
font-size: 2rem;
letter-spacing: -0.02em;
font-weight: 800;
color: #1f1f33;
}
.cards-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 2rem;
padding: 0 1rem;
margin-bottom: 4rem;
}
.feature-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: transform 0.2s, box-shadow 0.2s;
max-height: 800px;
overflow-y: auto;
}
.feature-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.card-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.25rem;
font-weight: 700;
color: #1f1f33;
}
.card-title i {
font-size: 1.5rem;
color: var(--primary-color);
}
.card-title.large-title {
font-size: 1.5rem;
}
.card-title.large-title i {
font-size: 1.75rem;
}
.form-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.integration-guide {
margin-bottom: 1rem;
}
.integration-guide h3 {
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 1.5rem;
border-bottom: 2px solid #3b82f6;
padding-bottom: 0.5rem;
}
.integration-guide h4 {
font-size: 1.1rem;
font-weight: 600;
color: #1e40af;
margin-bottom: 0.75rem;
}
.guide-section {
margin-bottom: 1.5rem;
}
.guide-section p {
color: #4b5563;
line-height: 1.6;
margin: 0.5rem 0;
}
.guide-section p.note {
font-size: 0.9rem;
color: #6b7280;
margin-top: 0.5rem;
}
.guide-section code {
background: #f3f4f6;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
color: #1e40af;
}
.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.5;
margin: 0.5rem 0;
}
.field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.field label {
font-weight: 600;
color: #4b5563;
font-size: 0.95rem;
}
:deep(.p-inputtext),
:deep(.p-dropdown),
:deep(.p-inputtextarea) {
border-radius: 8px;
border: 1px solid #d1d5db;
padding: 0.75rem;
}
:deep(.p-inputtext:focus),
:deep(.p-dropdown:focus),
:deep(.p-inputtextarea:focus) {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
:deep(.p-button) {
border-radius: 8px;
padding: 0.75rem;
font-weight: 600;
margin-top: 0.5rem;
}
:deep(.p-message) {
border-radius: 8px;
margin-top: 0.5rem;
}
@media (max-width: 768px) {
.cards-container {
grid-template-columns: 1fr;
padding: 0 0.5rem;
}
.main-title {
font-size: 1.5rem;
}
}
</style>