EngineWorkbench.vue 8.61 KB
<script setup lang="ts">
import { computed } from 'vue'

import EngineConsole from '@/components/engines/EngineConsole.vue'
import type { AppName, AppStatusInfo, ForumMessage, SystemStatus } from '@/types'
import { appLabel } from '@/utils/format'

const props = defineProps<{
  systemStatus: SystemStatus
  appStatus: Record<AppName, AppStatusInfo>
  activeApp: AppName
  activeEngineUrl: string
  consoleLines: string[]
  forumMessages: ForumMessage[]
  query: string
  busy: boolean
}>()

const emit = defineEmits<{
  'update:activeApp': [value: AppName]
  runSearch: []
  refreshConsole: []
}>()

const visibleMessages = computed(() => (
  props.forumMessages.length > 0
    ? props.forumMessages.slice(-4)
    : [
        {
          type: 'host',
          sender: 'System',
          content: '暂无 Forum 对话消息,分发研究指令后这里会展示多智能体讨论结果。',
          timestamp: '',
          source: 'forum',
        },
      ]
))

const contextEntities = computed(() => {
  const queryTokens = props.query
    .split(/[,。、“”‘’:;,.\s\n]+/)
    .map((item) => item.trim())
    .filter((item) => item.length >= 2)
    .slice(0, 4)

  const senderTokens = props.forumMessages
    .map((item) => item.sender)
    .filter(Boolean)
    .slice(0, 3)

  return [...new Set([...queryTokens, ...senderTokens])].slice(0, 6)
})

const activeReferences = computed(() => {
  const forumRefs = props.forumMessages
    .map((item) => item.source)
    .filter(Boolean)
    .slice(0, 3)
  const consoleRefs = props.consoleLines
    .slice(-2)
    .map((line) => line.slice(0, 64))

  return [...new Set([...forumRefs, ...consoleRefs])].filter(Boolean).slice(0, 4)
})

const suggestedPivot = computed(() => {
  if (props.query.trim()) {
    return `是否继续围绕“${props.query.slice(0, 22)}${props.query.length > 22 ? '…' : ''}”补充一个更细的子问题?`
  }
  return '可以先从当前研究对象生成一条更聚焦的子问题,再交给 Forum 进行协同研判。'
})

const activeEngineStatus = computed(() => props.appStatus[props.activeApp]?.status || 'stopped')
</script>

<template>
  <section class="engine-workbench archive-panel">
    <div class="engine-workbench__head">
      <div>
        <p class="archive-kicker">Active Workspace</p>
        <h3 class="archive-card-title">{{ appLabel(activeApp) }}</h3>
        <p class="archive-copy">
          {{ query || '当前还没有研究指令。建议先在任务中心完善上下文,再分发到运行中的引擎。' }}
        </p>
      </div>

      <div class="engine-workbench__actions">
        <span class="archive-chip" :data-tone="systemStatus.started ? 'success' : 'running'">
          <span class="archive-chip__dot" />
          {{ systemStatus.started ? 'Engine Cluster Online' : 'Await Engine Boot' }}
        </span>
        <button class="archive-button--ghost" type="button" :disabled="busy" @click="$emit('refreshConsole')">
          刷新日志
        </button>
        <button class="archive-button" type="button" :disabled="busy || !query" @click="$emit('runSearch')">
          分发研究指令
        </button>
      </div>
    </div>

    <div class="workspace-grid">
      <div class="workspace-stage">
        <div v-if="activeApp === 'forum'" class="forum-feed">
          <article
            v-for="message in visibleMessages"
            :key="`${message.timestamp}-${message.sender}-${message.content}`"
            class="forum-card"
            :class="{ 'forum-card--agent': message.type === 'agent' }"
          >
            <div class="forum-card__head">
              <span class="archive-kicker">{{ message.type === 'agent' ? 'AI Agent Synthesis' : 'Archivist Note' }}</span>
              <time>{{ message.timestamp || '实时' }}</time>
            </div>
            <strong>{{ message.sender }}</strong>
            <p>{{ message.content }}</p>
          </article>
        </div>

        <iframe
          v-else-if="activeEngineUrl && activeEngineStatus === 'running'"
          class="engine-frame"
          :src="activeEngineUrl"
          :title="appLabel(activeApp)"
        />

        <div v-else class="stage-empty">
          <strong>{{ appLabel(activeApp) }} 当前未运行</strong>
          <p class="archive-copy">先启动系统,或等待健康检查完成后再查看该引擎的实时界面。</p>
        </div>
      </div>

      <aside class="workspace-memory">
        <article class="memory-card">
          <div class="memory-card__head">
            <div>
              <p class="archive-kicker">Context Memory</p>
              <h4>当前研判上下文</h4>
            </div>
          </div>

          <div class="memory-block">
            <span class="archive-kicker">Extracted Entities</span>
            <div class="memory-tags">
              <span v-for="entity in contextEntities" :key="entity" class="memory-tag">{{ entity }}</span>
              <span v-if="contextEntities.length === 0" class="memory-tag">暂无实体</span>
            </div>
          </div>

          <div class="memory-block">
            <span class="archive-kicker">Active References</span>
            <ul class="reference-list">
              <li v-for="reference in activeReferences" :key="reference">{{ reference }}</li>
              <li v-if="activeReferences.length === 0">暂无可提取引用</li>
            </ul>
          </div>
        </article>

        <article class="pivot-card">
          <p class="archive-kicker">Suggested Pivot</p>
          <strong>下一步建议</strong>
          <span>{{ suggestedPivot }}</span>
          <button class="archive-button--quiet" type="button" :disabled="busy || !query" @click="$emit('runSearch')">
            发起子问题
          </button>
        </article>
      </aside>
    </div>

    <EngineConsole :title="appLabel(activeApp)" :lines="consoleLines" />
  </section>
</template>

<style scoped>
.engine-workbench,
.engine-workbench__head,
.engine-workbench__actions,
.workspace-grid,
.workspace-memory,
.memory-card,
.memory-block,
.memory-tags,
.forum-feed {
  display: grid;
  gap: 18px;
}

.engine-workbench__head {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
}

.engine-workbench__actions {
  grid-auto-flow: column;
  align-items: center;
}

.workspace-grid {
  grid-template-columns: minmax(0, 1.35fr) 340px;
  align-items: start;
}

.workspace-stage {
  min-height: 620px;
  border: 1px solid rgba(24, 35, 31, 0.08);
  border-radius: 28px;
  background: rgba(255, 255, 255, 0.72);
  overflow: hidden;
}

.forum-feed {
  padding: 24px;
}

.forum-card {
  display: grid;
  gap: 12px;
  padding: 20px;
  border-left: 2px solid rgba(22, 52, 45, 0.32);
  border-radius: 0 20px 20px 0;
  background: rgba(255, 251, 246, 0.88);
}

.forum-card--agent {
  border-left-color: rgba(141, 103, 56, 0.5);
  background: rgba(244, 238, 228, 0.9);
}

.forum-card__head {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  align-items: center;
}

.forum-card time {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-soft);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.forum-card strong,
.memory-card h4 {
  color: var(--primary);
}

.forum-card p,
.pivot-card span {
  margin: 0;
  line-height: 1.9;
  color: var(--text);
}

.engine-frame {
  width: 100%;
  min-height: 620px;
  border: 0;
  background: #fff;
}

.stage-empty {
  display: grid;
  place-items: center;
  gap: 10px;
  min-height: 620px;
  padding: 24px;
  text-align: center;
}

.memory-card,
.pivot-card {
  padding: 20px;
  border: 1px solid rgba(24, 35, 31, 0.08);
  border-radius: 24px;
  background: rgba(255, 255, 255, 0.72);
}

.memory-card__head h4,
.pivot-card strong {
  margin: 6px 0 0;
  font-size: 18px;
}

.memory-tags {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.memory-tag {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 8px 12px;
  border: 1px solid rgba(24, 35, 31, 0.08);
  border-radius: 16px;
  background: rgba(251, 247, 240, 0.84);
  color: var(--primary);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.reference-list {
  display: grid;
  gap: 10px;
  margin: 0;
  padding-left: 18px;
  color: var(--text-muted);
}

.pivot-card {
  display: grid;
  gap: 12px;
  background:
    radial-gradient(circle at top right, rgba(141, 103, 56, 0.12), transparent 24%),
    rgba(255, 251, 246, 0.88);
}

@media (max-width: 1280px) {
  .engine-workbench__head,
  .workspace-grid {
    grid-template-columns: 1fr;
  }

  .engine-workbench__actions {
    grid-auto-flow: row;
    justify-items: start;
  }
}

@media (max-width: 820px) {
  .memory-tags {
    grid-template-columns: 1fr;
  }
}
</style>