Spaces:
Running
Running
| <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> |