Files
CRM_Montzar/apps/web/src/components/AskMentoPanel.vue
T
MahdiandCursor 6c6e32e099 feat(web): adopt vuesax icons and shared skeleton loading
Replace the inline SVG icon set with the vuesax linear pack, add skeleton primitives for loading states, and keep page scroll inside the app shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-05 14:46:35 +03:30

130 lines
3.8 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue';
import { RouterLink } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
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 '@/icons/ant-bridge';
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,
});
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>