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>
130 lines
3.8 KiB
Vue
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>
|