major agent refactoring: wiki knowledge base, no RAG, no Qdrant, no Ollama

This commit is contained in:
2026-04-21 21:03:24 -04:00
parent 7e4b54d701
commit 44a1688657
80 changed files with 2699 additions and 4267 deletions

View File

@@ -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;

View File

@@ -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()"
>&ldquo;&rdquo;</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;

View 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>