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="{ active: isPaneOpen('dashboard') }"
|
||||
@click="togglePane('dashboard')"
|
||||
title="Dashboard"
|
||||
title="Display"
|
||||
>
|
||||
<RectangleGroupIcon class="w-4 h-4" />
|
||||
</button>
|
||||
@ -37,7 +37,7 @@
|
||||
class="toolbar-pill toolbar-pill-icon"
|
||||
:class="{ active: isPaneOpen('artifacts') }"
|
||||
@click="togglePane('artifacts')"
|
||||
title="Artifacts"
|
||||
title="Konsole"
|
||||
>
|
||||
<SparklesIcon class="w-4 h-4" />
|
||||
</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;
|
||||
},
|
||||
|
||||
// assay UI controls — store for later rendering
|
||||
controls(_data) {
|
||||
// TODO: render controls in workspace panel
|
||||
},
|
||||
// assay UI controls (legacy flat list) — kept for compat, ignored for now
|
||||
controls(_data) {},
|
||||
|
||||
// assay artifacts — store for later rendering
|
||||
artifacts(_data) {
|
||||
// TODO: render artifacts in workspace panel
|
||||
// assay artifacts — split into Display (pages/tables) and Konsole (machines/controls)
|
||||
artifacts(data) {
|
||||
chatStore.setArtifacts(data.artifacts || []);
|
||||
},
|
||||
|
||||
// assay session cleared
|
||||
|
||||
@ -16,9 +16,25 @@ export interface FinanceData {
|
||||
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', () => {
|
||||
// --- State ---
|
||||
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)
|
||||
const channelState = ref<ChannelState>('NO_SESSION');
|
||||
const connectionState = ref<ConnectionState>('CONNECTING');
|
||||
@ -66,6 +82,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
let _wsSend: ((payload: any) => void) | null = null;
|
||||
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)
|
||||
function newSession() {
|
||||
stashMessages();
|
||||
@ -201,6 +222,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
messages.value = [];
|
||||
sessionTotalTokens.value = null;
|
||||
finance.value = null;
|
||||
artifacts.value = [];
|
||||
resetStreamingState();
|
||||
}
|
||||
|
||||
@ -434,10 +456,16 @@ export const useChatStore = defineStore('chat', () => {
|
||||
handoverPending,
|
||||
isRunning,
|
||||
setWsSend,
|
||||
sendAction,
|
||||
newSession,
|
||||
handover,
|
||||
stop,
|
||||
confirmNew,
|
||||
stay,
|
||||
artifacts,
|
||||
displayArtifacts,
|
||||
konsoleArtifacts,
|
||||
setArtifacts,
|
||||
clearArtifacts,
|
||||
};
|
||||
});
|
||||
|
||||
@ -43,13 +43,13 @@
|
||||
/>
|
||||
</template>
|
||||
<template #dashboard>
|
||||
<DashboardPane />
|
||||
<DisplayPane />
|
||||
</template>
|
||||
<template #files>
|
||||
<FilesPane />
|
||||
</template>
|
||||
<template #artifacts>
|
||||
<ArtifactsPane />
|
||||
<KonsolePane />
|
||||
</template>
|
||||
</ContentLayout>
|
||||
|
||||
@ -79,9 +79,9 @@ import { usePanels } from '../composables/usePanels';
|
||||
import { LockClosedIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
||||
import ChatPane from '../components/ChatPane.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 ArtifactsPane from '../components/ArtifactsPane.vue';
|
||||
import KonsolePane from '../components/KonsolePane.vue';
|
||||
import DebugColumn from '../components/DebugColumn.vue';
|
||||
|
||||
import router from '../router';
|
||||
|
||||
Reference in New Issue
Block a user