Files
CRM_Montzar/apps/web/src/components/AskMentoPanel.vue
T

130 lines
3.8 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { ref } from 'vue';
import { RouterLink } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
2026-10-13 18:04:53 +03:30
import { rewriteGregorianDates } from '@/lib/jalali';
import { brand, type AskMentoResponse, type AskMentoSourceDto } from '@mento/shared';
import {
Alert as AAlert,
Button as AButton,
Card as ACard,
Flex as AFlex,
Input as AInput,
Space as ASpace,
Tag as ATag,
Typography,
} from 'ant-design-vue';
import { ThunderboltOutlined } from '@ant-design/icons-vue';
const { Paragraph: ATypographyParagraph } = Typography;
const props = defineProps<{
personId?: string;
placeholder?: string;
/** زمینهٔ گفتگو برای پاسخ مرتبط‌تر داخل چت */
chatContext?: string;
/** دکمهٔ درج پاسخ در پیش‌نویس پیام */
allowInsert?: boolean;
}>();
const emit = defineEmits<{
insertAnswer: [text: string];
}>();
const question = ref('');
const asking = ref(false);
const error = ref('');
const result = ref<AskMentoResponse | null>(null);
function sourceTo(source: AskMentoSourceDto) {
if (source.type === 'person') return { name: 'person-detail', params: { id: source.id } };
if (source.type === 'activity') return { name: 'activity-detail', params: { id: source.id } };
if (source.type === 'deal') return { name: 'deal-detail', params: { id: source.id } };
return { name: 'notes' };
}
async function submit() {
const q = question.value.trim();
if (!q || asking.value) return;
asking.value = true;
error.value = '';
result.value = null;
try {
const { data } = await api.post<AskMentoResponse>('/ai/ask', {
question: q,
personId: props.personId || undefined,
chatContext: props.chatContext?.trim() || undefined,
});
2026-10-13 18:04:53 +03:30
result.value = { ...data, answer: rewriteGregorianDates(data.answer) };
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'پرسش ناموفق بود');
} else {
error.value = 'پرسش ناموفق بود';
}
} finally {
asking.value = false;
}
}
function insertAnswer() {
const text = result.value?.answer?.trim();
if (!text) return;
emit('insertAnswer', text);
}
</script>
<template>
<a-flex vertical :gap="8" :style="{ minWidth: 0 }">
<a-flex :gap="8" align="center" :style="{ width: '100%' }">
<a-input
v-model:value="question"
allow-clear
:placeholder="placeholder || brand.askPlaceholderFa"
:style="{ flex: 1, minWidth: 0 }"
@pressEnter="submit"
>
<template #prefix>
<ThunderboltOutlined />
</template>
</a-input>
<a-button
type="primary"
:disabled="asking || question.trim().length < 2"
:loading="asking"
@click="submit"
>
بپرس
</a-button>
</a-flex>
<a-alert v-if="error" type="error" show-icon :message="error" />
<a-card v-else-if="result" :bordered="false">
<a-typography-paragraph :style="{ marginBottom: 0 }">
{{ result.answer }}
</a-typography-paragraph>
<a-space v-if="result.sources.length" wrap :size="6" :style="{ marginTop: '8px' }">
<RouterLink
v-for="source in result.sources"
:key="`${source.type}-${source.id}`"
:to="sourceTo(source)"
>
<a-tag color="cyan" :bordered="false">{{ source.label }}</a-tag>
</RouterLink>
</a-space>
<a-button
v-if="allowInsert && result.answer.trim()"
type="primary"
block
:style="{ marginTop: '8px' }"
@click="insertAnswer"
>
درج در پیام
</a-button>
</a-card>
</a-flex>
</template>