major agent refactoring: wiki knowledge base, no RAG, no Qdrant, no Ollama
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user