110 lines
3.3 KiB
Vue
110 lines
3.3 KiB
Vue
<script setup>
|
|
import { computed, ref, watch } from 'vue';
|
|
import { useRoute } from 'vue-router';
|
|
import CaptainAssistant from 'dashboard/api/captain/assistant';
|
|
|
|
const props = defineProps({
|
|
range: {
|
|
type: String,
|
|
default: '30',
|
|
},
|
|
});
|
|
|
|
const route = useRoute();
|
|
const assistantId = computed(() => route.params.assistantId);
|
|
|
|
// Markdown summary generated by the model from the assistant's stats (served by
|
|
// the captain/assistants/:id/summary endpoint). Numbers are emphasised with
|
|
// **bold** so we can highlight them.
|
|
const welcomeMarkdown = ref('');
|
|
const isLoading = ref(false);
|
|
|
|
const fetchSummary = async () => {
|
|
isLoading.value = true;
|
|
try {
|
|
const { data } = await CaptainAssistant.getSummary({
|
|
assistantId: assistantId.value,
|
|
range: props.range,
|
|
});
|
|
welcomeMarkdown.value = data.message ?? '';
|
|
} catch {
|
|
welcomeMarkdown.value = '';
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
watch([() => props.range, assistantId], fetchSummary, { immediate: true });
|
|
|
|
// Split a line on **bold** runs so we can render emphasised numbers as
|
|
// brand-highlighted spans with Tailwind (instead of styling raw HTML).
|
|
const parseSegments = text => {
|
|
const parts = [];
|
|
const regex = /\*\*(.+?)\*\*/g;
|
|
let lastIndex = 0;
|
|
let match = regex.exec(text);
|
|
while (match) {
|
|
if (match.index > lastIndex) {
|
|
parts.push({ text: text.slice(lastIndex, match.index) });
|
|
}
|
|
parts.push({ text: match[1], bold: true });
|
|
lastIndex = regex.lastIndex;
|
|
match = regex.exec(text);
|
|
}
|
|
if (lastIndex < text.length) {
|
|
parts.push({ text: text.slice(lastIndex) });
|
|
}
|
|
return parts.map((part, index) => ({ ...part, key: index }));
|
|
};
|
|
|
|
// Break the markdown into paragraphs on blank lines so newline breaks survive
|
|
// (a single <p> would collapse them), each parsed into highlightable segments.
|
|
const paragraphs = computed(() =>
|
|
welcomeMarkdown.value
|
|
.split(/\n{2,}/)
|
|
.map(block => block.trim())
|
|
.filter(Boolean)
|
|
.map((block, index) => ({ key: index, segments: parseSegments(block) }))
|
|
);
|
|
</script>
|
|
|
|
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
|
<template>
|
|
<div v-if="isLoading || welcomeMarkdown" class="flex flex-col gap-3">
|
|
<div class="flex items-center gap-1.5 text-n-slate-10">
|
|
<span class="i-lucide-sparkles size-3.5" />
|
|
<span class="text-xs">
|
|
{{ $t('CAPTAIN.OVERVIEW.WELCOME.LABEL') }}
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="isLoading"
|
|
class="flex flex-col gap-5"
|
|
:aria-label="$t('CAPTAIN.OVERVIEW.WELCOME.LOADING')"
|
|
>
|
|
<div class="flex flex-col gap-2.5">
|
|
<div class="w-full h-5 rounded bg-n-slate-3 animate-pulse" />
|
|
<div class="w-11/12 h-5 rounded bg-n-slate-3 animate-pulse" />
|
|
<div class="w-4/6 h-5 rounded bg-n-slate-3 animate-pulse" />
|
|
</div>
|
|
<div class="w-5/6 h-5 rounded bg-n-slate-3 animate-pulse" />
|
|
</div>
|
|
<template v-else>
|
|
<p
|
|
v-for="paragraph in paragraphs"
|
|
:key="paragraph.key"
|
|
class="text-lg leading-relaxed text-n-slate-12"
|
|
>
|
|
<template v-for="segment in paragraph.segments" :key="segment.key">
|
|
<span
|
|
v-if="segment.bold"
|
|
class="font-bold tabular-nums text-n-brand"
|
|
>{{ segment.text }}</span
|
|
>
|
|
<template v-else>{{ segment.text }}</template>
|
|
</template>
|
|
</p>
|
|
</template>
|
|
</div>
|
|
</template>
|