hesabixCore/webUI/src/views/wizard/home.vue
2025-07-22 08:55:13 +00:00

606 lines
16 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.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="chat-container">
<!-- ناحیه پیامها -->
<div class="messages-container" ref="messagesContainer">
<div
v-for="(message, index) in messages"
:key="index"
:class="['message', message.type]"
>
<div class="message-avatar">
<v-icon v-if="message.type === 'user'" size="24" color="white">mdi-account</v-icon>
<v-icon v-else size="24" color="primary">mdi-robot</v-icon>
</div>
<div class="message-content">
<div class="message-bubble">
<!-- تغییر: رندر داینامیک بر اساس نوع داده -->
<template v-if="message.type === 'ai' && message.data">
<div v-for="(item, idx) in message.data.data" :key="idx">
<div v-if="item.type === 'text'">{{ item.content }}</div>
<v-table v-else-if="item.type === 'table'" class="my-2" density="compact">
<thead>
<tr>
<th v-for="h in item.headers" :key="h">{{ h }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rIdx) in item.rows" :key="rIdx">
<td v-for="(cell, cIdx) in row" :key="cIdx">{{ cell }}</td>
</tr>
</tbody>
</v-table>
<AIChart v-else-if="item.type === 'chart' || item.chartType" :chartData="item" height="300" class="my-2" />
</div>
</template>
<template v-else>
<p class="message-text">{{ message.text }}</p>
</template>
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
</div>
</div>
</div>
<!-- نشانگر تایپ -->
<div v-if="isTyping" class="message ai">
<div class="message-avatar">
<v-icon size="24" color="primary">mdi-robot</v-icon>
</div>
<div class="message-content">
<div class="message-bubble typing">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
</div>
<!-- ناحیه ورودی -->
<div class="input-container">
<div class="input-wrapper">
<v-textarea
v-model="newMessage"
:placeholder="aiEnabled ? 'پیام خود را بنویسید...' : 'هوش مصنوعی غیرفعال است'"
variant="outlined"
rows="1"
auto-grow
hide-details
class="message-input"
:disabled="!aiEnabled"
@keydown.enter="sendMessage"
></v-textarea>
<v-btn
:disabled="!newMessage.trim() || isTyping || !aiEnabled"
color="primary"
icon
size="small"
class="send-button"
@click="sendMessage"
>
<v-icon>mdi-send</v-icon>
</v-btn>
</div>
<!-- دکمه‌های سریع -->
<div v-if="aiEnabled" class="quick-actions">
<v-chip
v-for="suggestion in quickSuggestions"
:key="suggestion"
variant="outlined"
size="small"
class="quick-chip"
@click="sendQuickMessage(suggestion)"
>
{{ suggestion }}
</v-chip>
</div>
<!-- نشانگر وضعیت -->
<div v-if="!aiEnabled && aiStatus !== 'checking'" class="status-indicator">
<v-alert
:type="aiStatus === 'error' ? 'error' : 'warning'"
variant="tonal"
density="compact"
class="status-alert"
>
{{ getStatusMessage(aiStatus, '') }}
</v-alert>
</div>
</div>
</div>
</template>
<script>
import { useNavigationStore } from '@/stores/navigationStore';
import axios from 'axios';
import AIChart from '@/components/widgets/AIChart.vue';
export default {
name: "WizardHome",
components: { AIChart },
data() {
return {
navigationStore: useNavigationStore(),
messages: [
{
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'سلام! من دستیار هوشمند شما هستم. چطور می‌تونم کمکتون کنم؟' }] },
timestamp: new Date()
}
],
newMessage: '',
isTyping: false,
aiEnabled: false,
aiStatus: 'checking',
conversationId: null,
quickSuggestions: [
'چطور می‌تونم کمکتون کنم؟',
'سوالی دارید؟',
'نیاز به راهنمایی دارید؟',
'مشکلی پیش اومده؟'
]
}
},
methods: {
async checkAIStatus() {
try {
this.aiStatus = 'checking';
const response = await axios.get('/api/wizard/status');
const data = response.data;
if (data.success) {
this.aiEnabled = data.status === 'available';
this.aiStatus = data.status;
if (!this.aiEnabled) {
// تغییر پیام اولیه بر اساس وضعیت
this.messages[0] = {
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: this.getStatusMessage(data.status, data.message) }] },
timestamp: new Date()
};
}
} else {
this.aiEnabled = false;
this.aiStatus = 'error';
this.messages[0] = {
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'خطا در بررسی وضعیت هوش مصنوعی. لطفاً دوباره تلاش کنید.' }] },
timestamp: new Date()
};
}
} catch (error) {
this.aiEnabled = false;
this.aiStatus = 'error';
this.messages[0] = {
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید.' }] },
timestamp: new Date()
};
}
},
getStatusMessage(status, message) {
switch (status) {
case 'disabled':
return 'سرویس هوش مصنوعی در حال حاضر غیرفعال است. لطفاً با مدیر سیستم تماس بگیرید.';
case 'no_api_key':
return 'کلید API هوش مصنوعی تنظیم نشده است. لطفاً با مدیر سیستم تماس بگیرید.';
case 'error':
return 'خطا در سرویس هوش مصنوعی. لطفاً دوباره تلاش کنید.';
default:
return message || 'سرویس هوش مصنوعی در دسترس نیست.';
}
},
async sendMessage() {
if (!this.newMessage.trim() || this.isTyping || !this.aiEnabled) return;
// اضافه کردن پیام کاربر
this.messages.push({
type: 'user',
text: this.newMessage.trim(),
timestamp: new Date()
});
const userMessage = this.newMessage.trim();
this.newMessage = '';
// اسکرول به پایین بعد از اضافه کردن پیام کاربر
setTimeout(() => {
this.scrollToBottom();
}, 100);
// شبیه‌سازی تایپ کردن AI
this.isTyping = true;
// اسکرول به پایین برای نشان دادن نشانگر تایپ
setTimeout(() => {
this.scrollToBottom();
}, 300);
try {
// ارسال پیام به سرور
const response = await axios.post('/api/wizard/talk', {
message: userMessage,
conversationId: this.conversationId || null
});
this.isTyping = false;
if (response.data.success) {
// --- تغییر: پردازش پاسخ JSON ---
let aiData = response.data.response;
let parsed = null;
try {
// اگر پاسخ داخل بلاک کد markdown است، فقط بخش json را جدا کن
if (typeof aiData === 'string' && aiData.trim().startsWith('```json')) {
aiData = aiData.replace(/^```json[\r\n]*/i, '').replace(/```$/i, '').trim();
}
// پارس چند مرحله‌ای تا رسیدن به آبجکت واقعی
parsed = aiData;
let safety = 0;
while (typeof parsed === 'string' && safety < 5) {
parsed = JSON.parse(parsed);
safety++;
}
// اگر باز هم data.data[0] رشته بود، دوباره پارس کن
if (
parsed &&
parsed.data &&
Array.isArray(parsed.data) &&
typeof parsed.data[0] === 'string'
) {
let safety2 = 0;
while (typeof parsed.data[0] === 'string' && safety2 < 5) {
parsed.data[0] = JSON.parse(parsed.data[0]);
safety2++;
}
}
} catch (e) {
// اگر JSON نبود، به صورت متن نمایش بده
parsed = { type: ['text'], data: [{ type: 'text', content: aiData }] };
}
console.debug('home.vue AI message parsed:', parsed);
this.messages.push({
type: 'ai',
data: parsed,
timestamp: new Date()
});
// ذخیره conversationId برای ادامه گفتگو
if (response.data.conversationId) {
this.conversationId = response.data.conversationId;
}
// نمایش اطلاعات هزینه در صورت وجود
if (response.data.cost) {
console.log('هزینه استفاده:', response.data.cost);
}
} else {
// نمایش خطا
this.messages.push({
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: `خطا: ${response.data.error}` }] },
timestamp: new Date()
});
}
// اسکرول به پایین بعد از دریافت پاسخ
setTimeout(() => {
this.scrollToBottom();
}, 200);
} catch (error) {
this.isTyping = false;
let errorMessage = 'خطا در ارتباط با سرور';
if (error.response) {
if (error.response.data && error.response.data.error) {
errorMessage = error.response.data.error;
} else if (error.response.status === 403) {
errorMessage = 'دسترسی غیرمجاز';
} else if (error.response.status === 500) {
errorMessage = 'خطای سرور';
}
} else if (error.request) {
errorMessage = 'خطا در اتصال به سرور';
}
this.messages.push({
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: errorMessage }] },
timestamp: new Date()
});
setTimeout(() => {
this.scrollToBottom();
}, 200);
}
},
async sendQuickMessage(suggestion) {
this.newMessage = suggestion;
await this.sendMessage();
},
generateAIResponse(userMessage) {
// این تابع دیگر استفاده نمی‌شود چون از API استفاده می‌کنیم
return 'پاسخ از سرور دریافت می‌شود';
},
formatTime(timestamp) {
return timestamp.toLocaleTimeString('fa-IR', {
hour: '2-digit',
minute: '2-digit'
});
},
scrollToBottom() {
this.$nextTick(() => {
setTimeout(() => {
const container = this.$refs.messagesContainer;
if (container) {
const rect = container.getBoundingClientRect();
const scrollTop = window.pageYOffset + rect.top + container.scrollHeight - window.innerHeight;
window.scrollTo({
top: scrollTop,
behavior: 'smooth'
});
}
}, 100);
});
}
},
async mounted() {
// بستن منو در دسکتاپ
if (!this.$vuetify.display.mobile) {
this.navigationStore.closeDrawer();
}
// بررسی وضعیت هوش مصنوعی
await this.checkAIStatus();
this.scrollToBottom();
},
beforeUnmount() {
// باز کردن منو در دسکتاپ
if (!this.$vuetify.display.mobile) {
this.navigationStore.openDrawer();
}
},
updated() {
this.scrollToBottom();
}
}
</script>
<style scoped>
.chat-container {
height: 100%;
display: flex;
flex-direction: column;
background: white;
}
.messages-container {
flex: 1;
padding: 24px;
padding-bottom: 160px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 0;
}
.message {
display: flex;
gap: 12px;
max-width: 80%;
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message.ai {
align-self: flex-start;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.message.user .message-avatar {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
}
.message.ai .message-avatar {
background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%);
border: 2px solid #e0e0e0;
}
.message-content {
flex: 1;
}
.message-bubble {
background: #f8f9fa;
padding: 14px 18px;
border-radius: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
position: relative;
border: 1px solid #e9ecef;
}
.message.user .message-bubble {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
color: white;
border: none;
}
.message-text {
margin: 0 0 4px 0;
line-height: 1.5;
font-size: 14px;
}
.message-time {
font-size: 11px;
opacity: 0.7;
display: block;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 8px 0;
}
.typing-indicator span {
width: 8px;
height: 8px;
background: #6c757d;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.input-container {
background: white;
padding: 20px 24px;
border-top: 1px solid #e9ecef;
box-shadow: 0 -4px 20px rgba(0,0,0,0.08);
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
}
.input-wrapper {
display: flex;
gap: 12px;
align-items: flex-end;
margin-bottom: 12px;
}
.message-input {
flex: 1;
}
.message-input :deep(.v-field__outline) {
border-radius: 24px;
border-color: #e9ecef;
}
.message-input :deep(.v-field--focused .v-field__outline) {
border-color: #1976d2;
}
.send-button {
border-radius: 50%;
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.3);
transition: all 0.3s ease;
}
.send-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.4);
}
.send-button .v-icon {
transform: rotate(180deg);
}
.quick-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.quick-chip {
cursor: pointer;
transition: all 0.3s ease;
border-color: #e9ecef;
color: #6c757d;
}
.quick-chip:hover {
background: #f8f9fa;
border-color: #1976d2;
color: #1976d2;
transform: translateY(-1px);
}
.status-indicator {
margin-top: 12px;
}
.status-alert {
border-radius: 12px;
font-size: 13px;
}
/* اسکرول‌بار سفارشی */
.messages-container::-webkit-scrollbar {
width: 6px;
}
.messages-container::-webkit-scrollbar-track {
background: transparent;
}
.messages-container::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.2);
border-radius: 3px;
}
.messages-container::-webkit-scrollbar-thumb:hover {
background: rgba(0,0,0,0.3);
}
/* ریسپانسیو */
@media (max-width: 768px) {
.messages-container {
padding: 16px;
padding-bottom: 160px;
}
.input-container {
padding: 12px 16px;
}
.message {
max-width: 90%;
}
}
</style>