bugfixes; research subproc; higher sandbox limits
This commit is contained in:
@@ -7,6 +7,7 @@ import TabPanels from 'primevue/tabpanels'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import OrdersTab from './tabs/OrdersTab.vue'
|
||||
import PlaceholderTab from './tabs/PlaceholderTab.vue'
|
||||
import ResearchTab from './tabs/ResearchTab.vue'
|
||||
|
||||
interface TempTab {
|
||||
id: string
|
||||
@@ -81,9 +82,10 @@ defineExpose({
|
||||
<template>
|
||||
<div class="bottom-tray" :style="trayStyle">
|
||||
<div v-if="isExpanded" class="tray-resize-handle" @pointerdown="startResize" @pointermove="onResizeMove" />
|
||||
<Tabs :value="activeTab" class="tray-tabs">
|
||||
<Tabs :value="isExpanded ? activeTab : null" class="tray-tabs">
|
||||
<TabList class="tray-tab-list">
|
||||
<Tab value="orders" @click="onTabClick('orders')">Orders</Tab>
|
||||
<Tab value="research" @click="onTabClick('research')">Research</Tab>
|
||||
<Tab value="strategies" @click="onTabClick('strategies')">Strategies</Tab>
|
||||
<Tab value="positions" @click="onTabClick('positions')">Positions</Tab>
|
||||
<Tab
|
||||
@@ -102,9 +104,10 @@ defineExpose({
|
||||
</button>
|
||||
</TabList>
|
||||
<TabPanels v-if="isExpanded" class="tray-panels">
|
||||
<TabPanel value="positions" class="tray-panel"><PlaceholderTab label="Positions" /></TabPanel>
|
||||
<TabPanel value="orders" class="tray-panel"><OrdersTab /></TabPanel>
|
||||
<TabPanel value="strategies" class="tray-panel"><PlaceholderTab label="Strategies" /></TabPanel>
|
||||
<TabPanel value="positions" class="tray-panel"><PlaceholderTab label="Positions" /></TabPanel>
|
||||
<TabPanel value="research" class="tray-panel"><ResearchTab /></TabPanel>
|
||||
<TabPanel
|
||||
v-for="tab in tempTabs"
|
||||
:key="tab.id"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { ref, onMounted, onBeforeUnmount, watch, type WatchStopHandle } from 'vue'
|
||||
import Card from 'primevue/card'
|
||||
import { createTradingViewDatafeed } from '../composables/useTradingViewDatafeed'
|
||||
import { useTradingViewShapes } from '../composables/useTradingViewShapes'
|
||||
@@ -11,10 +11,11 @@ import type { IChartingLibraryWidget } from '../types/tradingview'
|
||||
import { intervalToSeconds } from '../utils'
|
||||
import { wsManager } from '../composables/useWebSocket'
|
||||
|
||||
// Convert seconds to TradingView interval string
|
||||
// Convert seconds to TradingView interval string.
|
||||
// TradingView uses plain minute numbers ("60", "240") for intraday,
|
||||
// and "1D", "2D" etc for daily. Never use "H" suffix — it's not in supported_resolutions.
|
||||
function secondsToInterval(seconds: number): string {
|
||||
if (seconds % 86400 === 0) return `${seconds / 86400}D`
|
||||
if (seconds % 3600 === 0) return `${seconds / 3600}H`
|
||||
return `${seconds / 60}` // plain number = minutes
|
||||
}
|
||||
|
||||
@@ -27,9 +28,23 @@ let shapeCleanup: (() => void) | null = null // Cleanup function for shape sync
|
||||
let indicatorCleanup: (() => void) | null = null // Cleanup function for indicator sync
|
||||
let customIndicatorCleanup: (() => void) | null = null // Cleanup for custom TV studies
|
||||
let chartInitialized = false // Guard against double-init on reconnect
|
||||
let symbolWatcher: WatchStopHandle | null = null
|
||||
|
||||
const maybeInitChart = () => {
|
||||
if (chartInitialized || !chartContainer.value) return
|
||||
if (!chartStore.symbol) {
|
||||
// Defer until backend provides a symbol
|
||||
if (!symbolWatcher) {
|
||||
symbolWatcher = watch(() => chartStore.symbol, (sym) => {
|
||||
if (sym) {
|
||||
symbolWatcher?.()
|
||||
symbolWatcher = null
|
||||
maybeInitChart()
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
chartInitialized = true
|
||||
initChart()
|
||||
}
|
||||
@@ -205,6 +220,10 @@ function setupStoreWatchers() {
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (symbolWatcher) {
|
||||
symbolWatcher()
|
||||
symbolWatcher = null
|
||||
}
|
||||
// Cleanup shape synchronization
|
||||
if (shapeCleanup) {
|
||||
shapeCleanup()
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { ref, onMounted, onUnmounted, computed, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import { register } from 'vue-advanced-chat'
|
||||
import Badge from 'primevue/badge'
|
||||
import Button from 'primevue/button'
|
||||
import { wsManager } from '../composables/useWebSocket'
|
||||
import type { WebSocketMessage } from '../composables/useWebSocket'
|
||||
import { useChannelStore } from '../stores/channel'
|
||||
@@ -188,12 +187,13 @@ const handleMessage = (data: WebSocketMessage) => {
|
||||
}
|
||||
} else if (data.type === 'agent_chunk') {
|
||||
console.log('[ChatPanel] Processing agent_chunk, content:', data.content, 'done:', data.done)
|
||||
// Always remove any tool-call bubble when the agent sends text, whether this
|
||||
// is a new message or a continuation of an existing one (e.g. after a retry).
|
||||
removeToolCallBubble()
|
||||
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
|
||||
|
||||
if (!currentStreamingMessageId) {
|
||||
console.log('[ChatPanel] Starting new streaming message')
|
||||
// Remove any ephemeral tool-call bubble before starting the real response
|
||||
removeToolCallBubble()
|
||||
// Set up streaming state and mark user message as seen
|
||||
isAgentProcessing.value = true
|
||||
currentStreamingMessageId = generateMessageId()
|
||||
@@ -314,6 +314,7 @@ const stopAgent = () => {
|
||||
|
||||
// Send message handler
|
||||
const sendMessage = async (event: any) => {
|
||||
if (isAgentProcessing.value) { stopAgent(); return }
|
||||
// Extract data from CustomEvent.detail[0]
|
||||
const data = event.detail?.[0] || event
|
||||
|
||||
@@ -617,7 +618,11 @@ onUnmounted(() => {
|
||||
|
||||
<!-- Workspace loading overlay -->
|
||||
<div v-if="!channelStore.isReady" class="workspace-loading">
|
||||
<i class="pi pi-spin pi-spinner workspace-loading-spinner" />
|
||||
<svg class="workspace-loading-spinner" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="25" cy="25" r="20" fill="none" stroke="rgba(8,153,129,0.2)" stroke-width="4"/>
|
||||
<circle cx="25" cy="25" r="20" fill="none" stroke="#089981" stroke-width="4"
|
||||
stroke-dasharray="80 200" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="workspace-loading-message">{{ channelStore.statusMessage || 'Connecting...' }}</span>
|
||||
</div>
|
||||
|
||||
@@ -643,18 +648,18 @@ onUnmounted(() => {
|
||||
@send-message="sendMessage"
|
||||
@fetch-messages="fetchMessages"
|
||||
@open-file="openFile"
|
||||
/>
|
||||
|
||||
<!-- Stop button overlay -->
|
||||
<div v-if="isAgentProcessing" class="stop-button-container">
|
||||
<Button
|
||||
icon="pi pi-stop-circle"
|
||||
label="Stop"
|
||||
severity="danger"
|
||||
@click="stopAgent"
|
||||
class="stop-button"
|
||||
/>
|
||||
</div>
|
||||
>
|
||||
<div
|
||||
v-if="isAgentProcessing"
|
||||
slot="send-icon"
|
||||
@click.stop="stopAgent"
|
||||
style="display:flex;align-items:center;justify-content:center;width:100%;height:100%"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" fill="#f23645"/>
|
||||
</svg>
|
||||
</div>
|
||||
</vue-advanced-chat>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -682,8 +687,13 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.workspace-loading-spinner {
|
||||
font-size: 2rem;
|
||||
color: #089981;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
animation: workspace-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes workspace-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.workspace-loading-message {
|
||||
@@ -721,24 +731,4 @@ onUnmounted(() => {
|
||||
color: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.stop-button-container {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.stop-button {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
108
web/src/components/tabs/ResearchTab.vue
Normal file
108
web/src/components/tabs/ResearchTab.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useResearchTypesStore } from '../../stores/researchTypes'
|
||||
|
||||
const store = useResearchTypesStore()
|
||||
const { types } = storeToRefs(store)
|
||||
|
||||
const expanded = ref<Set<string>>(new Set())
|
||||
|
||||
const rows = computed(() =>
|
||||
Object.entries(types.value).map(([id, t]) => ({ id, ...t }))
|
||||
)
|
||||
|
||||
function toggle(id: string) {
|
||||
if (expanded.value.has(id)) {
|
||||
expanded.value.delete(id)
|
||||
} else {
|
||||
expanded.value.add(id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="research-tab">
|
||||
<div v-if="rows.length === 0" class="empty">No research items</div>
|
||||
<div v-for="row in rows" :key="row.id" class="research-row">
|
||||
<button class="row-header" @click="toggle(row.id)">
|
||||
<i class="pi" :class="expanded.has(row.id) ? 'pi-chevron-down' : 'pi-chevron-right'" />
|
||||
<span class="row-name">{{ row.display_name }}</span>
|
||||
<span class="row-id">{{ row.id }}</span>
|
||||
</button>
|
||||
<div v-if="expanded.has(row.id)" class="row-body">
|
||||
<span v-if="row.description">{{ row.description }}</span>
|
||||
<span v-else class="no-desc">No description</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.research-tab {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #555;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.research-row {
|
||||
border-bottom: 1px solid #1e1e1e;
|
||||
}
|
||||
|
||||
.row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: #dbdbdb;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.row-header:hover {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.row-header .pi {
|
||||
color: #666;
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.row-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.row-id {
|
||||
color: #555;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.row-body {
|
||||
padding: 6px 26px 8px;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
line-height: 1.5;
|
||||
background: #0d0d0d;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.no-desc {
|
||||
color: #444;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user