Newer
Older
navi-1 / webclient / src / components / messages / AssistantMessage.vue
<template>
  <div
    v-if="msg.thinking || msg.tools?.length || msg.text || !msg.done"
    class="msg-assistant"
  >
    <!-- Thinking block -->
    <ThinkingCard
      v-if="msg.thinking"
      :msg="msg"
    />

    <!-- Tool / plan cards -->
    <template v-for="(item, i) in msg.tools" :key="i">
      <ThinkingCard v-if="item.kind === 'turn_thinking'" :msg="item" />
      <details v-else-if="item.kind === 'plan'" class="plan-card" open>
        <summary>
          <i class="ph ph-map-trifold"></i>
          Plan
          <i class="ph ph-caret-down plan-chevron"></i>
        </summary>
        <div class="plan-body" v-html="renderMarkdown(item.text)" />
      </details>
      <ToolCard v-else :tool="item" />
    </template>

    <!-- Text response -->
    <div
      v-if="msg.text"
      ref="contentEl"
      class="msg-assistant-content"
      v-html="renderedText"
    />

    <!-- Streaming cursor -->
    <span v-if="!msg.done && msg.role === 'assistant'" class="stream-cursor" />
  </div>
</template>

<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import ThinkingCard from './ThinkingCard.vue'
import ToolCard from './ToolCard.vue'
import { renderMarkdown, attachCopyButtons } from '@/composables/useMarkdown.js'

const props = defineProps({ msg: { type: Object, required: true } })

const contentEl = ref(null)

const renderedText = computed(() => renderMarkdown(props.msg.text))

// Attach copy buttons after render
watch(renderedText, () => nextTick(() => attachCopyButtons(contentEl.value)))
</script>

<style scoped>
.stream-cursor {
  display: inline-block;
  width: 2px;
  height: 1em;
  background: currentColor;
  margin-left: 2px;
  vertical-align: text-bottom;
  animation: blink 1s step-end infinite;
}

@keyframes blink {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0; }
}
</style>