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

319 lines
8.0 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import type { NoteAiPayloadDto, NoteAiSuggestionDto } from '@mento/shared';
const props = defineProps<{
noteId: string;
hasPerson: boolean;
}>();
const emit = defineEmits<{
applied: [];
dismissed: [];
}>();
const loading = ref(false);
const applying = ref(false);
const error = ref('');
const suggestion = ref<NoteAiSuggestionDto | null>(null);
const applyNextAction = ref(true);
const selectedMemory = reactive<Record<number, boolean>>({});
const nextTitle = ref('');
const nextDueAt = ref('');
const memoryEdits = reactive<Record<number, string>>({});
const payload = computed(() => suggestion.value?.payload ?? null);
const isPending = computed(() => suggestion.value?.status === 'pending');
const memoryItems = computed(() => payload.value?.memoryItems ?? []);
watch(
() => props.noteId,
() => {
void load();
},
{ immediate: true },
);
watch(payload, (value) => {
if (!value) return;
applyNextAction.value = Boolean(value.nextAction);
nextTitle.value = value.nextAction?.title ?? '';
nextDueAt.value = value.nextAction?.dueAt
? value.nextAction.dueAt.slice(0, 16)
: '';
value.memoryItems.forEach((item, index) => {
selectedMemory[index] = true;
memoryEdits[index] = item.content;
});
});
async function load() {
loading.value = true;
error.value = '';
try {
const { data } = await api.get<NoteAiSuggestionDto | null>(
`/notes/${props.noteId}/ai-suggestion`,
);
suggestion.value = data;
} catch {
suggestion.value = null;
} finally {
loading.value = false;
}
}
async function analyze() {
loading.value = true;
error.value = '';
try {
const { data } = await api.post<NoteAiSuggestionDto>(
`/notes/${props.noteId}/analyze`,
);
suggestion.value = data;
} 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 = 'تحلیل ناموفق بود';
}
await load();
} finally {
loading.value = false;
}
}
async function apply() {
if (!payload.value || !isPending.value) return;
applying.value = true;
error.value = '';
try {
const memoryIndices = memoryItems.value
.map((_, index) => index)
.filter((index) => selectedMemory[index]);
await api.post(`/notes/${props.noteId}/ai-suggestion/apply`, {
applyNextAction: applyNextAction.value && Boolean(nextTitle.value.trim()),
memoryIndices: props.hasPerson ? memoryIndices : [],
nextAction: applyNextAction.value
? {
title: nextTitle.value.trim(),
description: payload.value.nextAction?.description,
dueAt: nextDueAt.value
? new Date(nextDueAt.value).toISOString()
: null,
}
: undefined,
memoryItems: memoryIndices.map((index) => ({
index,
content: memoryEdits[index]?.trim() || memoryItems.value[index]?.content,
})),
});
await load();
emit('applied');
} 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 {
applying.value = false;
}
}
async function dismiss() {
applying.value = true;
error.value = '';
try {
await api.post(`/notes/${props.noteId}/ai-suggestion/dismiss`);
await load();
emit('dismissed');
} catch {
error.value = 'رد پیشنهاد ناموفق بود';
} finally {
applying.value = false;
}
}
function formatPayloadSummary(value: NoteAiPayloadDto | null) {
if (!value?.summary) return '';
return value.summary;
}
</script>
<template>
<section class="ai-panel">
<header>
<strong>پیشنهاد Mento AI</strong>
<button
v-if="!isPending"
type="button"
class="link"
:disabled="loading"
@click="analyze"
>
{{ loading ? '…' : 'تحلیل مجدد' }}
</button>
<button
v-else-if="!suggestion"
type="button"
class="link"
:disabled="loading"
@click="analyze"
>
{{ loading ? 'در حال تحلیل…' : 'تحلیل با AI' }}
</button>
</header>
<p v-if="error" class="error">{{ error }}</p>
<p v-else-if="loading && !suggestion" class="muted">در حال تحلیل یادداشت…</p>
<p v-else-if="suggestion?.status === 'failed'" class="error">
{{ suggestion.errorMessage || 'تحلیل ناموفق بود' }}
<button type="button" class="link" @click="analyze">تلاش دوباره</button>
</p>
<p v-else-if="!suggestion" class="muted">
<button type="button" class="link" :disabled="loading" @click="analyze">
تحلیل این یادداشت با AI
</button>
</p>
<template v-else-if="isPending && payload">
<p v-if="formatPayloadSummary(payload)" class="summary">{{ formatPayloadSummary(payload) }}</p>
<div v-if="payload.nextAction" class="block">
<label class="check">
<input v-model="applyNextAction" type="checkbox" />
کار بعدی
</label>
<input v-model="nextTitle" type="text" placeholder="عنوان کار بعدی" />
<JalaliDateInput v-model="nextDueAt" mode="datetime" />
</div>
<div v-if="memoryItems.length" class="block">
<p class="block-title">حافظه مشتری (با تأیید شما ذخیره می‌شود)</p>
<p v-if="!hasPerson" class="warn">برای ذخیره حافظه، یادداشت باید به یک شخص متصل باشد.</p>
<label
v-for="(_, index) in memoryItems"
:key="index"
class="memory-row"
>
<input
v-model="selectedMemory[index]"
type="checkbox"
:disabled="!hasPerson"
/>
<textarea v-model="memoryEdits[index]" rows="2" />
</label>
</div>
<div class="actions">
<button type="button" class="btn" :disabled="applying" @click="apply">
{{ applying ? '…' : 'اعمال انتخاب‌شده' }}
</button>
<button type="button" class="btn btn-ghost" :disabled="applying" @click="dismiss">
رد کردن
</button>
</div>
</template>
<p v-else-if="suggestion?.status === 'applied'" class="muted ok">پیشنهاد اعمال شد.</p>
<p v-else-if="suggestion?.status === 'dismissed'" class="muted">پیشنهاد رد شد.</p>
</section>
</template>
<style scoped>
.ai-panel {
margin-top: 0.75rem;
padding: 0.75rem 0.85rem;
border: 1px dashed var(--accent);
border-radius: 12px;
background: rgba(255, 255, 255, 0.65);
display: grid;
gap: 0.6rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
.link {
border: none;
background: transparent;
color: var(--accent);
cursor: pointer;
font: inherit;
}
.block {
display: grid;
gap: 0.45rem;
}
.block-title {
margin: 0;
font-weight: 600;
}
.check {
display: flex;
align-items: center;
gap: 0.4rem;
}
.memory-row {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.45rem;
align-items: start;
}
input[type='text'],
textarea {
border: 1px solid var(--line);
border-radius: 10px;
padding: 0.55rem 0.7rem;
font: inherit;
width: 100%;
}
.actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.summary {
margin: 0;
color: var(--muted);
font-size: 0.92rem;
}
.muted {
margin: 0;
color: var(--muted);
}
.ok {
color: #067647;
}
.warn {
margin: 0;
color: #b54708;
font-size: 0.88rem;
}
.error {
margin: 0;
color: #b42318;
}
</style>