Port artifact renderers from cog frontend into Display/Konsole panes
- Rename DashboardPane → DisplayPane, ArtifactsPane → KonsolePane - Remove WorkspacePane (superseded by ContentLayout) - DisplayPane: renders data_table, entity_detail, document_page artifacts - KonsolePane: renders machine, action_bar, status artifacts - chat store: add artifacts ref, displayArtifacts/konsoleArtifacts computed, setArtifacts(), clearArtifacts(), sendAction() - useAgentSocket: wire artifacts event → chatStore.setArtifacts() - AppToolbar: update labels to Chat/Display/Files/Konsole Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
60029f1863
commit
8996e553e1
@ -21,7 +21,7 @@
|
|||||||
class="toolbar-pill toolbar-pill-icon"
|
class="toolbar-pill toolbar-pill-icon"
|
||||||
:class="{ active: isPaneOpen('dashboard') }"
|
:class="{ active: isPaneOpen('dashboard') }"
|
||||||
@click="togglePane('dashboard')"
|
@click="togglePane('dashboard')"
|
||||||
title="Dashboard"
|
title="Display"
|
||||||
>
|
>
|
||||||
<RectangleGroupIcon class="w-4 h-4" />
|
<RectangleGroupIcon class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -37,7 +37,7 @@
|
|||||||
class="toolbar-pill toolbar-pill-icon"
|
class="toolbar-pill toolbar-pill-icon"
|
||||||
:class="{ active: isPaneOpen('artifacts') }"
|
:class="{ active: isPaneOpen('artifacts') }"
|
||||||
@click="togglePane('artifacts')"
|
@click="togglePane('artifacts')"
|
||||||
title="Artifacts"
|
title="Konsole"
|
||||||
>
|
>
|
||||||
<SparklesIcon class="w-4 h-4" />
|
<SparklesIcon class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,115 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="artifacts-pane" :class="orientation">
|
|
||||||
<div class="artifacts-bar">
|
|
||||||
<span class="artifacts-label">Artifacts</span>
|
|
||||||
<button class="orient-btn" @click="toggleOrientation" :title="orientation === 'vertical' ? 'Switch to horizontal' : 'Switch to vertical'">
|
|
||||||
<component :is="orientation === 'vertical' ? Bars3Icon : Bars3BottomLeftIcon" class="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="artifacts-body">
|
|
||||||
<span class="artifacts-hint">saved artifacts</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'ArtifactsPane' });
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { Bars3Icon, Bars3BottomLeftIcon } from '@heroicons/vue/20/solid';
|
|
||||||
|
|
||||||
const orientation = ref<'vertical' | 'horizontal'>('vertical');
|
|
||||||
function toggleOrientation() {
|
|
||||||
orientation.value = orientation.value === 'vertical' ? 'horizontal' : 'vertical';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.artifacts-pane {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
min-width: 0;
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vertical: bar on top, body below */
|
|
||||||
.artifacts-pane.vertical {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.artifacts-pane.vertical .artifacts-bar {
|
|
||||||
flex-direction: row;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
padding: 0 8px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
.artifacts-pane.vertical .artifacts-body {
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Horizontal: bar on left, body to the right */
|
|
||||||
.artifacts-pane.horizontal {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.artifacts-pane.horizontal .artifacts-bar {
|
|
||||||
flex-direction: column;
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
padding: 8px 0;
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
.artifacts-pane.horizontal .artifacts-body {
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.artifacts-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: var(--bg-dim, var(--bg));
|
|
||||||
}
|
|
||||||
|
|
||||||
.artifacts-label {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
color: var(--text-dim);
|
|
||||||
opacity: 0.5;
|
|
||||||
writing-mode: initial;
|
|
||||||
}
|
|
||||||
.artifacts-pane.horizontal .artifacts-label {
|
|
||||||
writing-mode: vertical-rl;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.orient-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--text-dim);
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 0.12s;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.orient-btn:hover { opacity: 1; }
|
|
||||||
|
|
||||||
.artifacts-body {
|
|
||||||
display: flex;
|
|
||||||
opacity: 0.25;
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-size: 0.65rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="dashboard-pane">
|
|
||||||
<div class="dashboard-empty">
|
|
||||||
<span class="dashboard-label">Dashboard</span>
|
|
||||||
<span class="dashboard-hint">Artifacts and controls from the agent appear here</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'DashboardPane' });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dashboard-pane {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
.dashboard-empty {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: var(--text-dim);
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
.dashboard-label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
.dashboard-hint {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 160px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
212
src/components/DisplayPane.vue
Normal file
212
src/components/DisplayPane.vue
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<div class="display-pane">
|
||||||
|
|
||||||
|
<div v-if="!displayArtifacts.length" class="display-empty">
|
||||||
|
<span class="display-label">Anzeige</span>
|
||||||
|
<span class="display-hint">Tables, pages and cards appear here</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="display-scroll">
|
||||||
|
<template v-for="art in displayArtifacts" :key="art.id">
|
||||||
|
|
||||||
|
<!-- data_table -->
|
||||||
|
<div v-if="art.type === 'data_table'" class="ws-artifact ws-data-table">
|
||||||
|
<div v-if="art.data.title" class="ws-artifact-header">{{ art.data.title }}</div>
|
||||||
|
<div class="ws-table-wrap">
|
||||||
|
<table class="ws-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="col in art.data.columns" :key="col">{{ col }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, i) in (art.data.rows || art.data.data || [])" :key="i">
|
||||||
|
<td v-for="col in art.data.columns" :key="col">{{ row[col] ?? '' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div v-if="art.meta?.source" class="ws-artifact-meta">{{ art.meta.source }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- entity_detail: single card or list -->
|
||||||
|
<div v-else-if="art.type === 'entity_detail'" class="ws-artifact ws-entity-detail">
|
||||||
|
<div v-if="art.data.title" class="ws-card-title">{{ art.data.title }}</div>
|
||||||
|
<div v-if="art.data.subtitle" class="ws-card-subtitle">{{ art.data.subtitle }}</div>
|
||||||
|
|
||||||
|
<!-- list mode -->
|
||||||
|
<div v-if="art.data.items?.length" class="ws-list">
|
||||||
|
<div v-for="(item, i) in art.data.items" :key="i" class="ws-card-nested">
|
||||||
|
<div v-if="item.title" class="ws-card-title">{{ item.title }}</div>
|
||||||
|
<div v-if="item.fields?.length" class="ws-card-fields">
|
||||||
|
<div v-for="f in item.fields" :key="f.label" class="ws-card-field">
|
||||||
|
<span class="ws-card-key">{{ f.label }}</span>
|
||||||
|
<span class="ws-card-val">{{ f.value ?? '' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- single entity fields -->
|
||||||
|
<div v-if="art.data.fields?.length" class="ws-card-fields">
|
||||||
|
<div v-for="f in art.data.fields" :key="f.label" class="ws-card-field">
|
||||||
|
<span class="ws-card-key">{{ f.label }}</span>
|
||||||
|
<span v-if="f.action" class="ws-card-link" @click="sendAction(f.action)">{{ f.value ?? '' }}</span>
|
||||||
|
<span v-else class="ws-card-val">{{ f.value ?? '' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="art.actions?.length" class="ws-card-actions">
|
||||||
|
<button v-for="a in art.actions" :key="a.action" class="ws-btn" @click="sendAction(a.action, a.payload)">{{ a.label }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- document_page -->
|
||||||
|
<div v-else-if="art.type === 'document_page'" class="ws-artifact ws-document-page">
|
||||||
|
<div v-if="art.data.title" class="ws-doc-title">{{ art.data.title }}</div>
|
||||||
|
<div v-for="(section, i) in (art.data.sections || [])" :key="i" class="ws-doc-section">
|
||||||
|
<div v-if="section.heading" class="ws-doc-heading">{{ section.heading }}</div>
|
||||||
|
<div v-if="section.content" class="ws-doc-content" v-html="renderMd(section.content)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="art.actions?.length" class="ws-card-actions">
|
||||||
|
<button v-for="a in art.actions" :key="a.action" class="ws-btn" @click="sendAction(a.action, a.payload)">{{ a.label }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'DisplayPane' });
|
||||||
|
import { useChatStore } from '../store/chat';
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const { displayArtifacts, sendAction } = chatStore;
|
||||||
|
|
||||||
|
function renderMd(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||||
|
.replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.display-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.display-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
.display-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.display-hint { font-size: 0.72rem; text-align: center; max-width: 160px; }
|
||||||
|
|
||||||
|
.display-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-artifact {
|
||||||
|
background: var(--panel-bg, var(--bg-dim));
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ws-artifact-header {
|
||||||
|
padding: 8px 12px 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.ws-artifact-meta {
|
||||||
|
padding: 4px 12px 8px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.ws-table-wrap { overflow-x: auto; }
|
||||||
|
.ws-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
||||||
|
.ws-table th {
|
||||||
|
text-align: left; padding: 6px 10px; font-weight: 600;
|
||||||
|
color: var(--text-dim); border-bottom: 1px solid var(--border); white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ws-table td {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.ws-table tr:last-child td { border-bottom: none; }
|
||||||
|
.ws-table tr:hover td { background: color-mix(in srgb, var(--accent) 5%, transparent); }
|
||||||
|
|
||||||
|
/* Entity */
|
||||||
|
.ws-entity-detail { padding: 12px; }
|
||||||
|
.ws-card-title { font-size: 0.9rem; font-weight: 600; color: var(--text); margin-bottom: 2px; }
|
||||||
|
.ws-card-subtitle { font-size: 0.78rem; color: var(--text-dim); margin-bottom: 8px; }
|
||||||
|
.ws-list { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.ws-card-nested {
|
||||||
|
background: color-mix(in srgb, var(--border) 30%, transparent);
|
||||||
|
border-radius: 6px; padding: 8px 10px;
|
||||||
|
}
|
||||||
|
.ws-card-fields { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
|
||||||
|
.ws-card-field { display: flex; gap: 8px; font-size: 0.8rem; }
|
||||||
|
.ws-card-key { color: var(--text-dim); flex: 0 0 auto; min-width: 80px; }
|
||||||
|
.ws-card-val { color: var(--text); }
|
||||||
|
.ws-card-link { color: var(--accent); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
||||||
|
.ws-card-link:hover { opacity: 0.8; }
|
||||||
|
.ws-card-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
||||||
|
|
||||||
|
/* Document */
|
||||||
|
.ws-document-page { padding: 16px; }
|
||||||
|
.ws-doc-title { font-size: 1rem; font-weight: 700; color: var(--text); margin-bottom: 12px; }
|
||||||
|
.ws-doc-section { margin-bottom: 12px; }
|
||||||
|
.ws-doc-heading {
|
||||||
|
font-size: 0.85rem; font-weight: 600; color: var(--text-dim);
|
||||||
|
margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.ws-doc-content { font-size: 0.85rem; color: var(--text); line-height: 1.6; }
|
||||||
|
.ws-doc-content :deep(code) {
|
||||||
|
background: color-mix(in srgb, var(--border) 60%, transparent);
|
||||||
|
border-radius: 3px; padding: 1px 4px; font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shared button */
|
||||||
|
.ws-btn {
|
||||||
|
padding: 5px 12px;
|
||||||
|
background: var(--bg-dim);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
.ws-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
</style>
|
||||||
268
src/components/KonsolePane.vue
Normal file
268
src/components/KonsolePane.vue
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
<template>
|
||||||
|
<div class="konsole-pane" :class="orientation">
|
||||||
|
|
||||||
|
<div class="konsole-bar">
|
||||||
|
<span class="konsole-label">Konsole</span>
|
||||||
|
<button class="orient-btn" @click="toggleOrientation" :title="orientation === 'vertical' ? 'Switch to horizontal' : 'Switch to vertical'">
|
||||||
|
<component :is="orientation === 'vertical' ? Bars3Icon : Bars3BottomLeftIcon" class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="konsole-body">
|
||||||
|
|
||||||
|
<div v-if="!konsoleArtifacts.length" class="konsole-empty">
|
||||||
|
<span class="konsole-hint">Konsole</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="konsole-scroll">
|
||||||
|
<template v-for="art in konsoleArtifacts" :key="art.id">
|
||||||
|
|
||||||
|
<!-- machine -->
|
||||||
|
<div v-if="art.type === 'machine'" class="kw-artifact kw-machine">
|
||||||
|
<div class="kw-machine-header">
|
||||||
|
<span class="kw-machine-name">{{ art.data.machine_id }}</span>
|
||||||
|
<span class="kw-machine-state">{{ art.data.current }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="(text, i) in (art.data.content || [])" :key="i" class="kw-machine-content">{{ text }}</div>
|
||||||
|
<div v-if="Object.keys(art.data.stored_data || {}).length" class="kw-machine-data">
|
||||||
|
<span v-for="(v, k) in art.data.stored_data" :key="k" class="kw-machine-datum">{{ k }}={{ v }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="art.actions?.length" class="kw-actions">
|
||||||
|
<button
|
||||||
|
v-for="a in art.actions"
|
||||||
|
:key="a.action"
|
||||||
|
class="kw-btn"
|
||||||
|
@click="sendAction(a.action)"
|
||||||
|
>{{ a.label }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- action_bar -->
|
||||||
|
<div v-else-if="art.type === 'action_bar'" class="kw-artifact kw-action-bar">
|
||||||
|
<button
|
||||||
|
v-for="a in art.actions"
|
||||||
|
:key="a.action"
|
||||||
|
class="kw-btn"
|
||||||
|
@click="sendAction(a.action, a.payload)"
|
||||||
|
>{{ a.label }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- status -->
|
||||||
|
<div v-else-if="art.type === 'status'" class="kw-artifact kw-status" :class="'kw-dt-' + (art.data.display_type || 'text')">
|
||||||
|
<!-- progress bar -->
|
||||||
|
<template v-if="art.data.display_type === 'progress'">
|
||||||
|
<span class="kw-label">{{ art.data.label }}</span>
|
||||||
|
<div class="kw-bar"><div class="kw-fill" :style="{ width: clamp(art.data.value) + '%' }" /></div>
|
||||||
|
<span class="kw-pct">{{ clamp(art.data.value) }}%</span>
|
||||||
|
</template>
|
||||||
|
<!-- info -->
|
||||||
|
<template v-else-if="art.data.display_type === 'info'">
|
||||||
|
<span class="kw-icon">ℹ</span>
|
||||||
|
<span class="kw-label">{{ art.data.label }}</span>
|
||||||
|
</template>
|
||||||
|
<!-- text / default -->
|
||||||
|
<template v-else>
|
||||||
|
<span class="kw-label">{{ art.data.label }}</span>
|
||||||
|
<span v-if="art.data.value" class="kw-value">{{ art.data.value }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'KonsolePane' });
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Bars3Icon, Bars3BottomLeftIcon } from '@heroicons/vue/20/solid';
|
||||||
|
import { useChatStore } from '../store/chat';
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const { konsoleArtifacts, sendAction } = chatStore;
|
||||||
|
|
||||||
|
const orientation = ref<'vertical' | 'horizontal'>('vertical');
|
||||||
|
function toggleOrientation() {
|
||||||
|
orientation.value = orientation.value === 'vertical' ? 'horizontal' : 'vertical';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(v: any): number {
|
||||||
|
return Math.min(100, Math.max(0, Number(v) || 0));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.konsole-pane {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical: bar on top */
|
||||||
|
.konsole-pane.vertical { flex-direction: column; }
|
||||||
|
.konsole-pane.vertical .konsole-bar {
|
||||||
|
flex-direction: row;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.konsole-pane.vertical .konsole-body { flex: 1; overflow-y: auto; }
|
||||||
|
|
||||||
|
/* Horizontal: bar on left */
|
||||||
|
.konsole-pane.horizontal { flex-direction: row; }
|
||||||
|
.konsole-pane.horizontal .konsole-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding: 8px 0;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
.konsole-pane.horizontal .konsole-body { flex: 1; overflow-y: auto; }
|
||||||
|
|
||||||
|
.konsole-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--bg-dim, var(--bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.konsole-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.konsole-pane.horizontal .konsole-label {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orient-btn {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
width: 22px; height: 22px;
|
||||||
|
border: none; background: none; color: var(--text-dim);
|
||||||
|
cursor: pointer; border-radius: 4px;
|
||||||
|
opacity: 0.5; transition: opacity 0.12s; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.orient-btn:hover { opacity: 1; }
|
||||||
|
|
||||||
|
.konsole-body { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
|
.konsole-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.25;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.konsole-scroll {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Artifact base */
|
||||||
|
.kw-artifact {
|
||||||
|
background: var(--panel-bg, var(--bg-dim));
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Machine */
|
||||||
|
.kw-machine { padding: 10px; }
|
||||||
|
.kw-machine-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.kw-machine-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.kw-machine-state {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.kw-machine-content {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
.kw-machine-data {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.kw-machine-datum {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
background: color-mix(in srgb, var(--border) 40%, transparent);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action bar */
|
||||||
|
.kw-action-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status */
|
||||||
|
.kw-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.kw-label { color: var(--text-dim); flex: 0 0 auto; }
|
||||||
|
.kw-value { color: var(--text); font-weight: 500; }
|
||||||
|
.kw-icon { font-size: 0.85rem; }
|
||||||
|
.kw-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
background: color-mix(in srgb, var(--border) 60%, transparent);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.kw-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
.kw-pct { color: var(--text-dim); font-size: 0.75rem; flex: 0 0 auto; }
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.kw-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||||
|
.kw-btn {
|
||||||
|
padding: 5px 12px;
|
||||||
|
background: var(--bg-dim);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
.kw-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
</style>
|
||||||
@ -1,41 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="workspace-pane">
|
|
||||||
<div class="workspace-empty">
|
|
||||||
<span class="workspace-label">Workspace</span>
|
|
||||||
<span class="workspace-hint">Dashboard, files, artifacts</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'WorkspacePane' });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.workspace-pane {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
.workspace-empty {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: var(--text-dim);
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
.workspace-label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
.workspace-hint {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -56,14 +56,12 @@ export function useAgentSocket(
|
|||||||
if (data.session_id) chatStore.sessionKey = data.session_id;
|
if (data.session_id) chatStore.sessionKey = data.session_id;
|
||||||
},
|
},
|
||||||
|
|
||||||
// assay UI controls — store for later rendering
|
// assay UI controls (legacy flat list) — kept for compat, ignored for now
|
||||||
controls(_data) {
|
controls(_data) {},
|
||||||
// TODO: render controls in workspace panel
|
|
||||||
},
|
|
||||||
|
|
||||||
// assay artifacts — store for later rendering
|
// assay artifacts — split into Display (pages/tables) and Konsole (machines/controls)
|
||||||
artifacts(_data) {
|
artifacts(data) {
|
||||||
// TODO: render artifacts in workspace panel
|
chatStore.setArtifacts(data.artifacts || []);
|
||||||
},
|
},
|
||||||
|
|
||||||
// assay session cleared
|
// assay session cleared
|
||||||
|
|||||||
@ -16,9 +16,25 @@ export interface FinanceData {
|
|||||||
pricing: { prompt: number, completion: number };
|
pricing: { prompt: number, completion: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Artifact types that belong to each pane
|
||||||
|
const DISPLAY_TYPES = new Set(['data_table', 'document_page', 'entity_detail']);
|
||||||
|
const KONSOLE_TYPES = new Set(['machine', 'action_bar', 'status']);
|
||||||
|
|
||||||
export const useChatStore = defineStore('chat', () => {
|
export const useChatStore = defineStore('chat', () => {
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const messages = ref<any[]>([]);
|
const messages = ref<any[]>([]);
|
||||||
|
|
||||||
|
// --- Artifacts ---
|
||||||
|
const artifacts = ref<any[]>([]);
|
||||||
|
const displayArtifacts = computed(() => artifacts.value.filter(a => DISPLAY_TYPES.has(a.type)));
|
||||||
|
const konsoleArtifacts = computed(() => artifacts.value.filter(a => KONSOLE_TYPES.has(a.type)));
|
||||||
|
|
||||||
|
function setArtifacts(arts: any[]) {
|
||||||
|
artifacts.value = arts;
|
||||||
|
}
|
||||||
|
function clearArtifacts() {
|
||||||
|
artifacts.value = [];
|
||||||
|
}
|
||||||
// Two-SM architecture: channel (shared) + connection (per-user)
|
// Two-SM architecture: channel (shared) + connection (per-user)
|
||||||
const channelState = ref<ChannelState>('NO_SESSION');
|
const channelState = ref<ChannelState>('NO_SESSION');
|
||||||
const connectionState = ref<ConnectionState>('CONNECTING');
|
const connectionState = ref<ConnectionState>('CONNECTING');
|
||||||
@ -66,6 +82,11 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
let _wsSend: ((payload: any) => void) | null = null;
|
let _wsSend: ((payload: any) => void) | null = null;
|
||||||
function setWsSend(fn: (payload: any) => void) { _wsSend = fn; }
|
function setWsSend(fn: (payload: any) => void) { _wsSend = fn; }
|
||||||
|
|
||||||
|
// Artifact action dispatch — sends {type:'action', action, data} over WS
|
||||||
|
function sendAction(action: string, data: any = {}) {
|
||||||
|
_wsSend?.({ type: 'action', action, data });
|
||||||
|
}
|
||||||
|
|
||||||
// Session actions (called from HudControls / HudRow)
|
// Session actions (called from HudControls / HudRow)
|
||||||
function newSession() {
|
function newSession() {
|
||||||
stashMessages();
|
stashMessages();
|
||||||
@ -201,6 +222,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
messages.value = [];
|
messages.value = [];
|
||||||
sessionTotalTokens.value = null;
|
sessionTotalTokens.value = null;
|
||||||
finance.value = null;
|
finance.value = null;
|
||||||
|
artifacts.value = [];
|
||||||
resetStreamingState();
|
resetStreamingState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,10 +456,16 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
handoverPending,
|
handoverPending,
|
||||||
isRunning,
|
isRunning,
|
||||||
setWsSend,
|
setWsSend,
|
||||||
|
sendAction,
|
||||||
newSession,
|
newSession,
|
||||||
handover,
|
handover,
|
||||||
stop,
|
stop,
|
||||||
confirmNew,
|
confirmNew,
|
||||||
stay,
|
stay,
|
||||||
|
artifacts,
|
||||||
|
displayArtifacts,
|
||||||
|
konsoleArtifacts,
|
||||||
|
setArtifacts,
|
||||||
|
clearArtifacts,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -43,13 +43,13 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #dashboard>
|
<template #dashboard>
|
||||||
<DashboardPane />
|
<DisplayPane />
|
||||||
</template>
|
</template>
|
||||||
<template #files>
|
<template #files>
|
||||||
<FilesPane />
|
<FilesPane />
|
||||||
</template>
|
</template>
|
||||||
<template #artifacts>
|
<template #artifacts>
|
||||||
<ArtifactsPane />
|
<KonsolePane />
|
||||||
</template>
|
</template>
|
||||||
</ContentLayout>
|
</ContentLayout>
|
||||||
|
|
||||||
@ -79,9 +79,9 @@ import { usePanels } from '../composables/usePanels';
|
|||||||
import { LockClosedIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
import { LockClosedIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
||||||
import ChatPane from '../components/ChatPane.vue';
|
import ChatPane from '../components/ChatPane.vue';
|
||||||
import ContentLayout from '../components/ContentLayout.vue';
|
import ContentLayout from '../components/ContentLayout.vue';
|
||||||
import DashboardPane from '../components/DashboardPane.vue';
|
import DisplayPane from '../components/DisplayPane.vue';
|
||||||
import FilesPane from '../components/FilesPane.vue';
|
import FilesPane from '../components/FilesPane.vue';
|
||||||
import ArtifactsPane from '../components/ArtifactsPane.vue';
|
import KonsolePane from '../components/KonsolePane.vue';
|
||||||
import DebugColumn from '../components/DebugColumn.vue';
|
import DebugColumn from '../components/DebugColumn.vue';
|
||||||
|
|
||||||
import router from '../router';
|
import router from '../router';
|
||||||
|
|||||||
Reference in New Issue
Block a user