major agent refactoring: wiki knowledge base, no RAG, no Qdrant, no Ollama
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import ChartView from './components/ChartView.vue'
|
||||
import ChatPanel from './components/ChatPanel.vue'
|
||||
import LoginScreen from './components/LoginScreen.vue'
|
||||
@@ -20,44 +20,48 @@ const authError = ref<string>()
|
||||
const isMobile = ref(false)
|
||||
let stateSyncCleanup: (() => void) | null = null
|
||||
|
||||
// Horizontal split: chart width in pixels (initialized on mount)
|
||||
// Horizontal split: chat width in pixels (fixed, anchored to right edge)
|
||||
const CHART_MIN_PX = 300
|
||||
const CHAT_MIN_PX = 240
|
||||
const CHART_DEFAULT_RATIO = 0.62
|
||||
const chartWidth = ref(0)
|
||||
const CHAT_DEFAULT_RATIO = 0.38
|
||||
const chatWidth = ref(0)
|
||||
const windowWidth = ref(0)
|
||||
let hDragStartX = 0
|
||||
let hDragStartWidth = 0
|
||||
|
||||
function initChartWidth() {
|
||||
chartWidth.value = Math.round(window.innerWidth * CHART_DEFAULT_RATIO)
|
||||
const chartVisible = computed(() => windowWidth.value - chatWidth.value - 4 >= CHART_MIN_PX)
|
||||
|
||||
function initChatWidth() {
|
||||
windowWidth.value = window.innerWidth
|
||||
chatWidth.value = Math.round(window.innerWidth * CHAT_DEFAULT_RATIO)
|
||||
}
|
||||
|
||||
function startHDrag(e: PointerEvent) {
|
||||
e.preventDefault()
|
||||
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
||||
hDragStartX = e.clientX
|
||||
hDragStartWidth = chartWidth.value
|
||||
hDragStartWidth = chatWidth.value
|
||||
}
|
||||
|
||||
function onHDragMove(e: PointerEvent) {
|
||||
if (!e.buttons) return
|
||||
const delta = e.clientX - hDragStartX
|
||||
const maxWidth = window.innerWidth - CHAT_MIN_PX - 4
|
||||
chartWidth.value = Math.max(CHART_MIN_PX, Math.min(maxWidth, hDragStartWidth + delta))
|
||||
// dragging right shrinks chat; dragging left grows chat
|
||||
const delta = hDragStartX - e.clientX
|
||||
const maxChatWidth = windowWidth.value - CHART_MIN_PX - 4
|
||||
chatWidth.value = Math.max(CHAT_MIN_PX, Math.min(Math.max(maxChatWidth, CHAT_MIN_PX), hDragStartWidth + delta))
|
||||
}
|
||||
|
||||
// Clamp chartWidth so chart + chat always fit within the window
|
||||
function clampChartWidth() {
|
||||
const maxWidth = window.innerWidth - CHAT_MIN_PX - 4
|
||||
if (maxWidth >= CHART_MIN_PX) {
|
||||
chartWidth.value = Math.max(CHART_MIN_PX, Math.min(maxWidth, chartWidth.value))
|
||||
}
|
||||
// Clamp chatWidth so chat stays within valid range on resize
|
||||
function clampChatWidth() {
|
||||
windowWidth.value = window.innerWidth
|
||||
const maxChatWidth = windowWidth.value - CHART_MIN_PX - 4
|
||||
chatWidth.value = Math.max(CHAT_MIN_PX, Math.min(Math.max(maxChatWidth, CHAT_MIN_PX), chatWidth.value))
|
||||
}
|
||||
|
||||
// Check screen width for mobile layout
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
if (!isMobile.value) clampChartWidth()
|
||||
if (!isMobile.value) clampChatWidth()
|
||||
}
|
||||
|
||||
const chartStore = useChartStore()
|
||||
@@ -74,7 +78,7 @@ watch(isMobile, (mobile) => {
|
||||
|
||||
// Check if user is already authenticated on page load
|
||||
onMounted(async () => {
|
||||
initChartWidth()
|
||||
initChatWidth()
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
|
||||
@@ -149,11 +153,11 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
<div v-else-if="!isMobile" class="desktop-layout">
|
||||
<div class="top-area">
|
||||
<div class="chart-panel" :style="{ width: chartWidth + 'px' }">
|
||||
<div v-if="chartVisible" class="chart-panel">
|
||||
<ChartView />
|
||||
</div>
|
||||
<div class="h-grabber" @pointerdown="startHDrag" @pointermove="onHDragMove" />
|
||||
<div class="chat-panel">
|
||||
<div v-if="chartVisible" class="h-grabber" @pointerdown="startHDrag" @pointermove="onHDragMove" />
|
||||
<div class="chat-panel" :style="{ width: chatWidth + 'px' }">
|
||||
<ChatPanel />
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +193,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.chart-panel {
|
||||
flex-shrink: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
@@ -209,8 +214,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DetailsEditDialog from './DetailsEditDialog.vue'
|
||||
import ResearchViewDialog from './ResearchViewDialog.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
category: 'indicator' | 'strategy' | 'research'
|
||||
@@ -10,11 +11,19 @@ const props = defineProps<{
|
||||
const dialogVisible = ref(false)
|
||||
const editingName = ref('')
|
||||
|
||||
const viewDialogVisible = ref(false)
|
||||
const viewingName = ref('')
|
||||
|
||||
function openEdit(name: string) {
|
||||
editingName.value = name
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openView(name: string) {
|
||||
viewingName.value = name
|
||||
viewDialogVisible.value = true
|
||||
}
|
||||
|
||||
function onUpdated(_payload: { category: string; name: string; success: boolean; error?: string }) {
|
||||
// Hook for handling the details_updated response — add logic here as needed
|
||||
}
|
||||
@@ -26,10 +35,18 @@ function onUpdated(_payload: { category: string; name: string; success: boolean;
|
||||
<div v-for="row in rows" :key="row.id" class="item-row">
|
||||
<span class="item-name">{{ row.display_name }}</span>
|
||||
<span class="item-desc">{{ row.description ?? '' }}</span>
|
||||
<button v-if="category === 'research'" class="view-btn" @click="openView(row.display_name)">View</button>
|
||||
<button class="edit-btn" @click="openEdit(row.display_name)">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResearchViewDialog
|
||||
v-if="category === 'research'"
|
||||
v-model:visible="viewDialogVisible"
|
||||
:category="category"
|
||||
:name="viewingName"
|
||||
/>
|
||||
|
||||
<DetailsEditDialog
|
||||
v-model:visible="dialogVisible"
|
||||
:category="category"
|
||||
@@ -88,6 +105,23 @@ function onUpdated(_payload: { category: string; name: string; success: boolean;
|
||||
mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%);
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: 1px solid #3d3d3d;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
border-color: #4a9eca;
|
||||
color: #4a9eca;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
|
||||
@@ -131,6 +131,93 @@ function close() {
|
||||
<i class="pi pi-exclamation-triangle" /> {{ loadError }}
|
||||
</div>
|
||||
<div class="editor-wrap">
|
||||
<div v-if="editor" class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('bold') }"
|
||||
title="Bold"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
><b>B</b></button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('italic') }"
|
||||
title="Italic"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
><i>I</i></button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('strike') }"
|
||||
title="Strikethrough"
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
><s>S</s></button>
|
||||
</div>
|
||||
<div class="toolbar-sep" />
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('heading', { level: 1 }) }"
|
||||
title="Heading 1"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||
>H1</button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('heading', { level: 2 }) }"
|
||||
title="Heading 2"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
>H2</button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('heading', { level: 3 }) }"
|
||||
title="Heading 3"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
>H3</button>
|
||||
</div>
|
||||
<div class="toolbar-sep" />
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('bulletList') }"
|
||||
title="Bullet list"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
><i class="pi pi-list" /></button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('orderedList') }"
|
||||
title="Ordered list"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
><i class="pi pi-sort-amount-down" /></button>
|
||||
</div>
|
||||
<div class="toolbar-sep" />
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('code') }"
|
||||
title="Inline code"
|
||||
@click="editor.chain().focus().toggleCode().run()"
|
||||
><code style="font-size:11px">`c`</code></button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('codeBlock') }"
|
||||
title="Code block"
|
||||
@click="editor.chain().focus().toggleCodeBlock().run()"
|
||||
><i class="pi pi-code" /></button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
:class="{ active: editor.isActive('blockquote') }"
|
||||
title="Blockquote"
|
||||
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||
>“”</button>
|
||||
</div>
|
||||
<div class="toolbar-sep" />
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
class="tb-btn"
|
||||
title="Horizontal rule"
|
||||
@click="editor.chain().focus().setHorizontalRule().run()"
|
||||
>—</button>
|
||||
</div>
|
||||
</div>
|
||||
<EditorContent :editor="editor" class="tiptap-editor" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -206,6 +293,59 @@ function close() {
|
||||
overflow: auto;
|
||||
background: #0d0d0d;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 5px 8px;
|
||||
border-bottom: 1px solid #2e2e2e;
|
||||
background: #111;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.toolbar-sep {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: #2e2e2e;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.tb-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #8a8a8a;
|
||||
cursor: pointer;
|
||||
padding: 3px 7px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
min-width: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.1s, color 0.1s;
|
||||
}
|
||||
|
||||
.tb-btn:hover {
|
||||
background: #1e1e1e;
|
||||
color: #dbdbdb;
|
||||
}
|
||||
|
||||
.tb-btn.active {
|
||||
background: #1e3a3f;
|
||||
color: #89d4e0;
|
||||
}
|
||||
|
||||
.confirm-overlay {
|
||||
@@ -245,6 +385,11 @@ function close() {
|
||||
|
||||
<style>
|
||||
/* Global (unscoped) so TipTap's .ProseMirror gets styled */
|
||||
.details-dialog .tiptap-editor {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.details-dialog .tiptap-editor .ProseMirror {
|
||||
padding: 12px 14px;
|
||||
outline: none;
|
||||
|
||||
294
web/src/components/ResearchViewDialog.vue
Normal file
294
web/src/components/ResearchViewDialog.vue
Normal file
@@ -0,0 +1,294 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Markdown } from 'tiptap-markdown'
|
||||
import { wsManager, type WebSocketMessage } from '../composables/useWebSocket'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
category: string
|
||||
name: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
}>()
|
||||
|
||||
type LoadState = 'idle' | 'loading' | 'ready' | 'error' | 'empty'
|
||||
|
||||
const loadState = ref<LoadState>('idle')
|
||||
const loadError = ref('')
|
||||
const images = ref<Array<{ mimeType: string; data: string }>>([])
|
||||
const lightboxSrc = ref<string | null>(null)
|
||||
|
||||
// Keep editable:true so setContent works reliably; read-only appearance is via CSS
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Markdown.configure({ html: false, transformPastedText: false }),
|
||||
],
|
||||
content: '',
|
||||
})
|
||||
|
||||
watch(() => props.visible, (v) => {
|
||||
if (v) {
|
||||
loadState.value = 'loading'
|
||||
loadError.value = ''
|
||||
images.value = []
|
||||
lightboxSrc.value = null
|
||||
editor.value?.commands.setContent('')
|
||||
wsManager.send({ type: 'read_output', category: props.category, name: props.name })
|
||||
} else {
|
||||
loadState.value = 'idle'
|
||||
images.value = []
|
||||
lightboxSrc.value = null
|
||||
editor.value?.commands.setContent('')
|
||||
}
|
||||
})
|
||||
|
||||
const messageHandler = (msg: WebSocketMessage) => {
|
||||
if (msg.category !== props.category || msg.name !== props.name) return
|
||||
|
||||
if (msg.type === 'output_data') {
|
||||
const hasAnalysis = !!msg.analysis
|
||||
const hasImages = Array.isArray(msg.images) && msg.images.length > 0
|
||||
if (!hasAnalysis && !hasImages) {
|
||||
loadState.value = 'empty'
|
||||
return
|
||||
}
|
||||
if (hasAnalysis) {
|
||||
editor.value?.commands.setContent(msg.analysis ?? '')
|
||||
}
|
||||
images.value = hasImages ? msg.images : []
|
||||
loadState.value = 'ready'
|
||||
} else if (msg.type === 'output_error') {
|
||||
loadError.value = msg.error ?? 'Failed to load output'
|
||||
loadState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
wsManager.addHandler(messageHandler)
|
||||
onUnmounted(() => wsManager.removeHandler(messageHandler))
|
||||
|
||||
function openLightbox(src: string) {
|
||||
lightboxSrc.value = src
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
lightboxSrc.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:visible="visible"
|
||||
:header="`Output — ${name}`"
|
||||
:modal="true"
|
||||
:closable="true"
|
||||
:style="{ width: '800px', maxWidth: '95vw' }"
|
||||
class="research-view-dialog"
|
||||
@update:visible="emit('update:visible', false)"
|
||||
>
|
||||
<div class="dialog-body">
|
||||
<div v-if="loadState === 'loading'" class="state-msg">
|
||||
<i class="pi pi-spin pi-spinner" /> Loading output…
|
||||
</div>
|
||||
<div v-else-if="loadState === 'error'" class="state-msg error">
|
||||
<i class="pi pi-exclamation-triangle" /> {{ loadError }}
|
||||
</div>
|
||||
<div v-else-if="loadState === 'empty'" class="state-msg muted">
|
||||
No output yet — run the script first.
|
||||
</div>
|
||||
<div v-else-if="loadState === 'ready'" class="content-scroll">
|
||||
<img
|
||||
v-for="(img, idx) in images"
|
||||
:key="idx"
|
||||
:src="`data:${img.mimeType};base64,${img.data}`"
|
||||
class="output-image"
|
||||
:alt="`Chart ${idx + 1}`"
|
||||
@click="openLightbox(`data:${img.mimeType};base64,${img.data}`)"
|
||||
/>
|
||||
<EditorContent v-if="editor" :editor="editor" class="tiptap-viewer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox overlay -->
|
||||
<Teleport to="body">
|
||||
<div v-if="lightboxSrc" class="lightbox-overlay" @click="closeLightbox">
|
||||
<img :src="lightboxSrc" class="lightbox-img" alt="Full size" @click.stop />
|
||||
<button class="lightbox-close" @click="closeLightbox">
|
||||
<i class="pi pi-times" />
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.state-msg {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #8a8a8a;
|
||||
font-size: 13px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.state-msg.error { color: #e06c6c; }
|
||||
.state-msg.muted { color: #555; }
|
||||
|
||||
.content-scroll {
|
||||
overflow-y: auto;
|
||||
max-height: 65vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-image {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #2e2e2e;
|
||||
display: block;
|
||||
cursor: zoom-in;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.output-image:hover {
|
||||
border-color: #4a9eca;
|
||||
}
|
||||
|
||||
.lightbox-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
.lightbox-img {
|
||||
max-width: 92vw;
|
||||
max-height: 92vh;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 20px;
|
||||
background: rgba(30, 30, 30, 0.9);
|
||||
border: 1px solid #3d3d3d;
|
||||
color: #aaa;
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.lightbox-close:hover {
|
||||
color: #fff;
|
||||
border-color: #666;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Global so TipTap's .ProseMirror gets styled */
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror {
|
||||
padding: 12px 14px;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #dbdbdb;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
/* Read-only appearance without disabling setContent */
|
||||
caret-color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror h1,
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror h2,
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror h3 {
|
||||
color: #eaeaea;
|
||||
margin: 0.8em 0 0.4em;
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror h1 { font-size: 1.3em; }
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror h2 { font-size: 1.15em; }
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror p { margin: 0 0 0.6em; }
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror strong {
|
||||
color: #eaeaea;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror em {
|
||||
color: #bbb;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror code {
|
||||
background: #1a1a1a;
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
font-size: 0.9em;
|
||||
color: #89d4e0;
|
||||
}
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror pre {
|
||||
background: #141414;
|
||||
border: 1px solid #2e2e2e;
|
||||
border-radius: 4px;
|
||||
padding: 10px 14px;
|
||||
overflow-x: auto;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #dbdbdb;
|
||||
}
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror ul,
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror ol {
|
||||
padding-left: 1.4em;
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror blockquote {
|
||||
border-left: 3px solid #3d3d3d;
|
||||
margin: 0.6em 0;
|
||||
padding-left: 12px;
|
||||
color: #8a8a8a;
|
||||
}
|
||||
|
||||
.research-view-dialog .tiptap-viewer .ProseMirror hr {
|
||||
border: none;
|
||||
border-top: 1px solid #2e2e2e;
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -553,6 +553,7 @@ export function useCustomIndicators(tvWidget: any) {
|
||||
// ------------------------------------------------------------------
|
||||
async function registerCustomStudy(indicator: IndicatorInstance) {
|
||||
const meta = indicator.custom_metadata
|
||||
?? indicatorTypesStore.types[indicator.pandas_ta_name]?.metadata
|
||||
if (!meta) {
|
||||
console.warn('[CustomIndicators] No custom_metadata on indicator:', indicator.id)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user