hesabixCore/webUI/src/views/wizard/home.vue

1115 lines
34 KiB
Vue
Raw Normal View History

2025-04-12 18:50:34 +03:30
<template>
2025-07-21 03:32:19 +03:30
<div class="chat-container">
<!-- لودینگ بالای صفحه برای عملیات گفتگو -->
<v-progress-linear
v-if="loadingConversation"
color="primary"
indeterminate
absolute
style="top:0; left:0; right:0; z-index:2000;"
/>
<!-- دیالوگ مدیریت گفتوگوها - ساختار استاندارد Vuetify -->
<v-dialog v-model="showConversations" max-width="420" scrollable transition="dialog-bottom-transition">
<v-card>
<v-toolbar color="primary" dark flat rounded>
<v-avatar color="white" size="36" class="mr-3"><v-icon color="primary">mdi-forum</v-icon></v-avatar>
<v-toolbar-title class="font-weight-bold">مدیریت گفتوگوها</v-toolbar-title>
<v-spacer></v-spacer>
<!-- دکمه حذف همه گفتوگوها با تولتیپ -->
<v-tooltip text="حذف همه گفت‌وگوها" location="bottom">
<template #activator="{ props }">
<v-btn
v-bind="props"
color="error"
:loading="loadingDeleteAll"
icon
class="ml-1"
@click="showDeleteAllDialog = true"
>
<v-icon>mdi-delete-sweep</v-icon>
</v-btn>
</template>
</v-tooltip>
<!-- دکمه بستن دیالوگ -->
<v-btn icon variant="text" @click="showConversations = false"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar>
<v-divider></v-divider>
<v-list class="py-0" style="min-height: 320px; max-height: 60vh; overflow-y: auto;">
<template v-if="conversations.length">
<template v-for="(conv, idx) in conversations" :key="conv.id">
<v-list-item :active="conv.id === conversationId" @click="switchConversation(conv.id)" class="conv-list-item-v">
<template #prepend>
<v-avatar :color="getAvatarColor(conv.title)" size="36">
<span class="white--text text-h6">{{ conv.title.charAt(0) }}</span>
</v-avatar>
</template>
<v-list-item-title class="font-weight-bold">{{ conv.title }}</v-list-item-title>
<v-list-item-subtitle class="d-flex align-center">
<v-icon size="16" color="grey" class="ml-1">mdi-clock-outline</v-icon>
<span class="mr-1">{{ conv.updatedAt }}</span>
<v-divider vertical class="mx-2" style="height: 18px;"></v-divider>
<v-icon size="16" color="primary" class="ml-1">mdi-message-reply-text</v-icon>
<span>{{ conv.messageCount }}</span>
</v-list-item-subtitle>
<template #append>
<v-btn icon color="error" variant="text" @click.stop="confirmDelete(conv.id)"><v-icon>mdi-delete</v-icon></v-btn>
</template>
</v-list-item>
<v-divider v-if="idx !== conversations.length - 1" class="my-1"></v-divider>
</template>
</template>
<template v-else>
<div class="d-flex flex-column align-center justify-center py-10">
<v-icon size="64" color="grey-lighten-1">mdi-emoticon-happy-outline</v-icon>
<div class="mt-3 text-h6">هنوز گفتوگویی ایجاد نکردهاید!</div>
<div class="text-caption mt-1">برای شروع، روی دکمه زیر کلیک کنید.</div>
</div>
</template>
</v-list>
<v-divider></v-divider>
<v-card-actions class="pa-4 d-flex flex-row-reverse align-center justify-space-between">
<v-btn color="primary" block large class="font-weight-bold" @click="createConversation">
<v-icon start>mdi-plus</v-icon>
گفتوگوی جدید
</v-btn>
<!-- دکمه حذف همه را از پایین (v-card-actions) حذف کن -->
</v-card-actions>
<!-- دیالوگ تایید حذف -->
<v-dialog v-model="showDeleteDialog" max-width="320">
<v-card>
<v-card-title class="text-h6">حذف گفتگو</v-card-title>
<v-card-text>آیا از حذف این گفتگو مطمئن هستید؟ این عمل قابل بازگشت نیست.</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showDeleteDialog = false">انصراف</v-btn>
<v-btn color="error" variant="flat" :loading="loadingDelete" :disabled="loadingDelete" @click="doDeleteConversation">حذف</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- دیالوگ تایید حذف همه -->
<v-dialog v-model="showDeleteAllDialog" max-width="340">
<v-card>
<v-card-title class="text-h6">حذف همه گفتوگوها</v-card-title>
<v-card-text>آیا از حذف همه گفتوگوها مطمئن هستید؟ این عمل قابل بازگشت نیست.</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showDeleteAllDialog = false">انصراف</v-btn>
<v-btn color="error" variant="flat" :loading="loadingDeleteAll" :disabled="loadingDeleteAll" @click="deleteAllConversations">حذف همه</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</v-dialog>
2025-07-21 03:32:19 +03:30
<!-- ناحیه پیامها -->
<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>
2025-07-18 06:29:39 +03:30
</div>
2025-07-21 03:32:19 +03:30
<div class="message-content">
<div class="message-bubble">
2025-07-22 12:25:13 +03:30
<!-- تغییر: رندر داینامیک بر اساس نوع داده -->
<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>
2025-07-21 03:32:19 +03:30
<p class="message-text">{{ message.text }}</p>
2025-07-22 12:25:13 +03:30
</template>
2025-07-21 03:32:19 +03:30
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
2025-07-19 16:34:23 +03:30
</div>
2025-07-21 03:32:19 +03:30
</div>
2025-07-19 16:34:23 +03:30
</div>
2025-07-21 03:32:19 +03:30
<!-- نشانگر تایپ -->
<div v-if="isTyping" class="message ai">
<div class="message-avatar">
<v-icon size="24" color="primary">mdi-robot</v-icon>
2025-07-18 06:29:39 +03:30
</div>
2025-07-21 03:32:19 +03:30
<div class="message-content">
<div class="message-bubble typing">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
2025-07-18 02:30:04 +03:30
</div>
2025-05-04 01:07:39 +03:30
</div>
2025-07-19 16:34:23 +03:30
</div>
</div>
2025-05-04 01:07:39 +03:30
</div>
2025-07-18 06:29:39 +03:30
2025-07-21 03:32:19 +03:30
<!-- ناحیه ورودی -->
<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"
2025-07-18 06:29:39 +03:30
>
2025-07-21 03:32:19 +03:30
<v-icon>mdi-send</v-icon>
2025-07-18 06:29:39 +03:30
</v-btn>
<!-- دکمه مدیریت گفتوگوها فقط آیکون با تولتیپ -->
<v-tooltip text="مدیریت گفت‌وگوها" location="top">
<template #activator="{ props }">
<v-btn
v-bind="props"
color="info"
size="small"
class="send-button"
@click="showConversations = true"
icon
>
<v-icon>mdi-forum</v-icon>
</v-btn>
</template>
</v-tooltip>
2025-07-21 03:32:19 +03:30
</div>
2025-07-18 06:29:39 +03:30
2025-07-21 03:32:19 +03:30
<!-- دکمههای سریع -->
<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)"
2025-07-18 06:29:39 +03:30
>
2025-07-21 03:32:19 +03:30
{{ suggestion }}
</v-chip>
</div>
2025-07-18 19:59:35 +03:30
2025-07-21 03:32:19 +03:30
<!-- نشانگر وضعیت -->
<div v-if="!aiEnabled && aiStatus !== 'checking'" class="status-indicator">
<v-alert
:type="aiStatus === 'error' ? 'error' : 'warning'"
variant="tonal"
density="compact"
class="status-alert"
2025-07-19 16:34:23 +03:30
>
2025-07-21 03:32:19 +03:30
{{ getStatusMessage(aiStatus, '') }}
</v-alert>
</div>
</div>
<v-snackbar v-model="showSnackbar" :color="snackbarColor" location="bottom" timeout="3500">
{{ snackbarText }}
</v-snackbar>
2025-07-21 03:32:19 +03:30
</div>
2025-04-12 18:50:34 +03:30
</template>
<script>
2025-07-21 03:32:19 +03:30
import { useNavigationStore } from '@/stores/navigationStore';
2025-07-18 06:29:39 +03:30
import axios from 'axios';
2025-07-22 12:25:13 +03:30
import AIChart from '@/components/widgets/AIChart.vue';
import { h } from 'vue';
2025-07-18 02:30:04 +03:30
2025-04-12 18:50:34 +03:30
export default {
2025-07-21 03:32:19 +03:30
name: "WizardHome",
2025-07-22 12:25:13 +03:30
components: { AIChart },
2025-04-12 18:50:34 +03:30
data() {
return {
2025-07-21 03:32:19 +03:30
navigationStore: useNavigationStore(),
messages: [
{
type: 'ai',
2025-07-22 12:25:13 +03:30
data: { type: ['text'], data: [{ type: 'text', content: 'سلام! من دستیار هوشمند شما هستم. چطور می‌تونم کمکتون کنم؟' }] },
2025-07-21 03:32:19 +03:30
timestamp: new Date()
}
],
newMessage: '',
2025-07-18 06:29:39 +03:30
isTyping: false,
2025-07-21 03:32:19 +03:30
aiEnabled: false,
aiStatus: 'checking',
conversationId: null,
showConversations: false,
conversations: [],
2025-07-21 03:32:19 +03:30
quickSuggestions: [
'چگونه می‌توانم یک گزارش مالی تهیه کنم؟',
'برای ثبت یک فاکتور جدید چه مراحلی را باید طی کنم؟',
'چطور می‌توانم کاربران جدید به سیستم اضافه کنم؟',
'چگونه می‌توانم تنظیمات کسب‌وکارم را تغییر دهم؟'
],
showDeleteDialog: false,
deleteTargetId: null,
loadingConversation: false,
loadingDelete: false,
showSnackbar: false,
snackbarText: '',
snackbarColor: 'success',
showDeleteAllDialog: false,
loadingDeleteAll: false,
2025-04-12 18:50:34 +03:30
}
},
methods: {
2025-07-21 03:32:19 +03:30
async checkAIStatus() {
2025-07-18 02:30:04 +03:30
try {
2025-07-21 03:32:19 +03:30
this.aiStatus = 'checking';
2025-07-18 06:29:39 +03:30
const response = await axios.get('/api/wizard/status');
2025-07-21 03:32:19 +03:30
const data = response.data;
if (data.success) {
this.aiEnabled = data.status === 'available';
this.aiStatus = data.status;
if (!this.aiEnabled) {
// تغییر پیام اولیه بر اساس وضعیت
this.messages[0] = {
type: 'ai',
2025-07-22 12:25:13 +03:30
data: { type: ['text'], data: [{ type: 'text', content: this.getStatusMessage(data.status, data.message) }] },
2025-07-21 03:32:19 +03:30
timestamp: new Date()
};
}
} else {
this.aiEnabled = false;
this.aiStatus = 'error';
this.messages[0] = {
type: 'ai',
2025-07-22 12:25:13 +03:30
data: { type: ['text'], data: [{ type: 'text', content: 'خطا در بررسی وضعیت هوش مصنوعی. لطفاً دوباره تلاش کنید.' }] },
2025-07-21 03:32:19 +03:30
timestamp: new Date()
};
2025-07-18 02:30:04 +03:30
}
} catch (error) {
2025-07-21 03:32:19 +03:30
this.aiEnabled = false;
this.aiStatus = 'error';
this.messages[0] = {
type: 'ai',
2025-07-22 12:25:13 +03:30
data: { type: ['text'], data: [{ type: 'text', content: 'خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید.' }] },
2025-07-21 03:32:19 +03:30
timestamp: new Date()
2025-07-18 06:29:39 +03:30
};
}
},
2025-07-21 03:32:19 +03:30
getStatusMessage(status, message) {
switch (status) {
case 'disabled':
return 'سرویس هوش مصنوعی در حال حاضر غیرفعال است. لطفاً با مدیر سیستم تماس بگیرید.';
case 'no_api_key':
return 'کلید API هوش مصنوعی تنظیم نشده است. لطفاً با مدیر سیستم تماس بگیرید.';
case 'error':
return 'خطا در سرویس هوش مصنوعی. لطفاً دوباره تلاش کنید.';
default:
return message || 'سرویس هوش مصنوعی در دسترس نیست.';
}
2025-07-18 06:29:39 +03:30
},
2025-07-21 03:32:19 +03:30
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);
2025-07-18 06:29:39 +03:30
try {
2025-07-21 03:32:19 +03:30
// ارسال پیام به سرور
const response = await axios.post('/api/wizard/talk', {
message: userMessage,
conversationId: this.conversationId || null
});
this.isTyping = false;
2025-07-18 06:29:39 +03:30
if (response.data.success) {
2025-07-22 12:25:13 +03:30
// --- تغییر: پردازش پاسخ 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);
2025-07-21 03:32:19 +03:30
this.messages.push({
type: 'ai',
2025-07-22 12:25:13 +03:30
data: parsed,
2025-07-21 03:32:19 +03:30
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',
2025-07-22 12:25:13 +03:30
data: { type: ['text'], data: [{ type: 'text', content: `خطا: ${response.data.error}` }] },
2025-07-21 03:32:19 +03:30
timestamp: new Date()
});
2025-07-18 02:30:04 +03:30
}
2025-07-21 03:32:19 +03:30
// اسکرول به پایین بعد از دریافت پاسخ
setTimeout(() => {
this.scrollToBottom();
}, 200);
2025-07-18 06:29:39 +03:30
} catch (error) {
2025-07-21 03:32:19 +03:30
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 = 'خطا در اتصال به سرور';
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
this.messages.push({
type: 'ai',
2025-07-22 12:25:13 +03:30
data: { type: ['text'], data: [{ type: 'text', content: errorMessage }] },
2025-07-21 03:32:19 +03:30
timestamp: new Date()
});
setTimeout(() => {
2025-07-18 06:29:39 +03:30
this.scrollToBottom();
2025-07-21 03:32:19 +03:30
}, 200);
2025-05-04 01:07:39 +03:30
}
},
2025-07-21 03:32:19 +03:30
async sendQuickMessage(suggestion) {
this.newMessage = suggestion;
await this.sendMessage();
2025-07-19 16:34:23 +03:30
},
2025-07-21 03:32:19 +03:30
generateAIResponse(userMessage) {
// این تابع دیگر استفاده نمی‌شود چون از API استفاده می‌کنیم
return 'پاسخ از سرور دریافت می‌شود';
2025-07-19 16:34:23 +03:30
},
2025-07-21 03:32:19 +03:30
2025-07-19 16:34:23 +03:30
2025-07-21 03:32:19 +03:30
formatTime(timestamp) {
return timestamp.toLocaleTimeString('fa-IR', {
hour: '2-digit',
minute: '2-digit'
});
2025-07-19 16:34:23 +03:30
},
2025-07-21 03:32:19 +03:30
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 fetchConversations() {
const res = await axios.post('/api/wizard/conversations/list');
if (res.data.success) {
this.conversations = res.data.items;
}
},
async createConversation() {
this.loadingConversation = true;
const res = await axios.post('/api/wizard/conversations/create', { title: 'گفتگوی جدید' });
if (res.data.success) {
this.showConversations = false;
this.conversationId = res.data.id;
this.messages = [
{
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'سلام! گفت‌وگوی جدید ایجاد شد. پیام خود را بنویسید.' }] },
timestamp: new Date()
}
];
await this.fetchConversations();
}
this.loadingConversation = false;
},
async switchConversation(id) {
this.loadingConversation = true;
this.conversationId = id;
this.showConversations = false;
// دریافت پیام‌های گفتگو
const res = await axios.post(`/api/wizard/conversations/${id}/messages`);
if (res.data.success) {
this.messages = res.data.items.map(msg => {
const role = (msg.role || '').toLowerCase();
if (role.includes('user')) {
return {
type: 'user',
text: msg.content,
timestamp: new Date(msg.createdAt)
};
} else if (role.includes('ai') || role.includes('assistant') || role.includes('system')) {
let parsed = null;
try {
parsed = msg.content;
let safety = 0;
while (typeof parsed === 'string' && safety < 5) {
parsed = JSON.parse(parsed);
safety++;
}
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) {
parsed = { type: ['text'], data: [{ type: 'text', content: msg.content }] };
}
return {
type: 'ai',
data: parsed,
timestamp: new Date(msg.createdAt)
};
} else {
// پیش‌فرض: پیام کاربر
return {
type: 'user',
text: msg.content,
timestamp: new Date(msg.createdAt)
};
}
});
}
this.loadingConversation = false;
},
async deleteConversation(id) {
this.loadingConversation = true;
await axios.post(`/api/wizard/conversations/${id}/delete`);
await this.fetchConversations();
if (this.conversationId === id) {
this.conversationId = null;
this.messages = [
{
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'گفت‌وگو حذف شد. یک گفت‌وگوی جدید شروع کنید.' }] },
timestamp: new Date()
}
];
}
this.loadingConversation = false;
},
confirmDelete(id) {
this.deleteTargetId = id;
this.showDeleteDialog = true;
},
async doDeleteConversation() {
if (this.deleteTargetId) {
this.loadingDelete = true;
try {
await this.deleteConversation(this.deleteTargetId);
this.showDeleteDialog = false;
this.deleteTargetId = null;
this.snackbarText = 'گفت‌وگو با موفقیت حذف شد.';
this.snackbarColor = 'success';
this.showSnackbar = true;
} catch (e) {
this.snackbarText = 'خطا در حذف گفت‌وگو!';
this.snackbarColor = 'error';
this.showSnackbar = true;
}
this.loadingDelete = false;
}
},
async deleteAllConversations() {
this.loadingDeleteAll = true;
try {
const res = await axios.post('/api/wizard/conversations/delete-all');
if (res.data.success) {
this.showDeleteAllDialog = false;
this.snackbarText = 'همه گفت‌وگوها با موفقیت حذف شدند.';
this.snackbarColor = 'success';
this.showSnackbar = true;
await this.fetchConversations();
this.conversationId = null;
this.messages = [
{
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'همه گفت‌وگوها حذف شدند. یک گفت‌وگوی جدید شروع کنید.' }] },
timestamp: new Date()
}
];
} else {
this.snackbarText = 'خطا در حذف همه گفت‌وگوها!';
this.snackbarColor = 'error';
this.showSnackbar = true;
}
} catch (e) {
this.snackbarText = 'خطا در حذف همه گفت‌وگوها!';
this.snackbarColor = 'error';
this.showSnackbar = true;
}
this.loadingDeleteAll = false;
},
getAvatarColor(title) {
// تولید رنگ ثابت بر اساس عنوان گفتگو
const colors = ['primary', 'deep-purple', 'indigo', 'teal', 'cyan', 'pink', 'orange', 'green', 'blue', 'red'];
let hash = 0;
for (let i = 0; i < title.length; i++) hash = title.charCodeAt(i) + ((hash << 5) - hash);
return colors[Math.abs(hash) % colors.length];
},
renderLastMessagePreview(msg) {
if (!msg || typeof msg !== 'string') return {
render() { return h('span', msg || 'بدون پیام'); }
};
let parsed;
try {
parsed = JSON.parse(msg);
} catch (e) {
return {
render() { return h('span', msg); }
};
}
if (parsed && parsed.type && parsed.data && Array.isArray(parsed.data)) {
if (parsed.type.includes('text')) {
const textItem = parsed.data.find(d => d.type === 'text');
if (textItem && textItem.content) {
return {
render() { return h('span', textItem.content); }
};
}
}
if (parsed.type.includes('table')) {
const tableItem = parsed.data.find(d => d.type === 'table');
if (tableItem && tableItem.headers && tableItem.rows) {
return {
render() {
return h('v-simple-table', { class: 'conv-preview-table' }, [
h('thead', [
h('tr', tableItem.headers.map(hd => h('th', hd)))
]),
h('tbody', tableItem.rows.map(row =>
h('tr', row.map(cell => h('td', cell)))
))
]);
}
};
}
}
}
return {
render() { return h('span', msg); }
};
},
2025-07-21 03:32:19 +03:30
},
async mounted() {
// بستن منو در دسکتاپ
if (!this.$vuetify.display.mobile) {
this.navigationStore.closeDrawer();
}
// بررسی وضعیت هوش مصنوعی
await this.checkAIStatus();
await this.fetchConversations();
2025-07-21 03:32:19 +03:30
this.scrollToBottom();
},
beforeUnmount() {
// باز کردن منو در دسکتاپ
if (!this.$vuetify.display.mobile) {
this.navigationStore.openDrawer();
}
},
updated() {
this.scrollToBottom();
}
}
</script>
2025-07-19 16:34:23 +03:30
2025-07-21 03:32:19 +03:30
<style scoped>
.chat-container {
height: 100%;
display: flex;
flex-direction: column;
background: white;
}
2025-07-19 16:34:23 +03:30
2025-07-18 06:29:39 +03:30
2025-07-21 03:32:19 +03:30
.messages-container {
flex: 1;
padding: 24px;
padding-bottom: 160px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 0;
}
2025-07-18 06:29:39 +03:30
2025-07-21 03:32:19 +03:30
.message {
display: flex;
gap: 12px;
max-width: 80%;
}
2025-07-18 06:29:39 +03:30
2025-07-21 03:32:19 +03:30
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
2025-05-04 01:07:39 +03:30
2025-07-21 03:32:19 +03:30
.message.ai {
align-self: flex-start;
}
2025-07-18 07:15:33 +03:30
2025-07-21 03:32:19 +03:30
.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);
}
2025-07-18 02:30:04 +03:30
2025-07-21 03:32:19 +03:30
.message.user .message-avatar {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
}
2025-07-18 06:29:39 +03:30
2025-07-21 03:32:19 +03:30
.message.ai .message-avatar {
background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%);
border: 2px solid #e0e0e0;
}
2025-05-04 01:07:39 +03:30
2025-07-21 03:32:19 +03:30
.message-content {
2025-07-18 06:29:39 +03:30
flex: 1;
2025-05-04 01:07:39 +03:30
}
2025-07-21 03:32:19 +03:30
.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;
2025-05-04 01:07:39 +03:30
}
2025-07-21 03:32:19 +03:30
.message.user .message-bubble {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
color: white;
border: none;
2025-04-12 18:50:34 +03:30
}
2025-07-21 03:32:19 +03:30
.message-text {
margin: 0 0 4px 0;
line-height: 1.5;
font-size: 14px;
2025-04-12 18:50:34 +03:30
}
2025-07-21 03:32:19 +03:30
.message-time {
font-size: 11px;
opacity: 0.7;
display: block;
2025-05-04 01:07:39 +03:30
}
2025-07-21 03:32:19 +03:30
.typing-indicator {
2025-07-19 16:34:23 +03:30
display: flex;
2025-07-21 03:32:19 +03:30
gap: 4px;
padding: 8px 0;
2025-04-12 18:50:34 +03:30
}
2025-07-21 03:32:19 +03:30
.typing-indicator span {
width: 8px;
height: 8px;
background: #6c757d;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
2025-04-12 18:50:34 +03:30
}
2025-07-21 03:32:19 +03:30
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
2025-07-18 02:30:04 +03:30
2025-07-21 03:32:19 +03:30
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
.input-container {
2025-07-18 06:29:39 +03:30
background: white;
2025-07-21 03:32:19 +03:30
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;
2025-07-18 02:30:04 +03:30
}
2025-07-21 03:32:19 +03:30
.input-wrapper {
2025-07-19 16:34:23 +03:30
display: flex;
2025-07-21 03:32:19 +03:30
gap: 12px;
align-items: flex-end;
margin-bottom: 12px;
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
.message-input {
2025-07-19 16:34:23 +03:30
flex: 1;
}
2025-07-21 03:32:19 +03:30
.message-input :deep(.v-field__outline) {
border-radius: 24px;
border-color: #e9ecef;
2025-05-04 01:07:39 +03:30
}
2025-07-21 03:32:19 +03:30
.message-input :deep(.v-field--focused .v-field__outline) {
border-color: #1976d2;
2025-04-12 18:50:34 +03:30
}
2025-07-21 03:32:19 +03:30
.send-button {
border-radius: 50%;
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.3);
transition: all 0.3s ease;
2025-07-18 06:29:39 +03:30
}
2025-07-21 03:32:19 +03:30
.send-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.4);
2025-05-04 01:07:39 +03:30
}
2025-07-21 03:32:19 +03:30
.send-button .v-icon {
transform: rotate(180deg);
2025-05-04 01:07:39 +03:30
}
2025-07-21 03:32:19 +03:30
.quick-actions {
2025-07-19 16:34:23 +03:30
display: flex;
2025-07-21 03:32:19 +03:30
gap: 8px;
flex-wrap: wrap;
2025-04-12 18:50:34 +03:30
}
2025-07-18 02:30:04 +03:30
2025-07-21 03:32:19 +03:30
.quick-chip {
cursor: pointer;
2025-07-19 16:34:23 +03:30
transition: all 0.3s ease;
2025-07-21 03:32:19 +03:30
border-color: #e9ecef;
color: #6c757d;
2025-07-18 02:30:04 +03:30
}
2025-07-21 03:32:19 +03:30
.quick-chip:hover {
background: #f8f9fa;
border-color: #1976d2;
color: #1976d2;
2025-07-19 16:34:23 +03:30
transform: translateY(-1px);
}
2025-07-21 03:32:19 +03:30
.status-indicator {
margin-top: 12px;
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
.status-alert {
border-radius: 12px;
font-size: 13px;
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
/* اسکرول‌بار سفارشی */
.messages-container::-webkit-scrollbar {
width: 6px;
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
.messages-container::-webkit-scrollbar-track {
background: transparent;
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
.messages-container::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.2);
border-radius: 3px;
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
.messages-container::-webkit-scrollbar-thumb:hover {
background: rgba(0,0,0,0.3);
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
/* ریسپانسیو */
2025-07-19 16:34:23 +03:30
@media (max-width: 768px) {
.messages-container {
padding: 16px;
2025-07-21 03:32:19 +03:30
padding-bottom: 160px;
2025-07-19 16:34:23 +03:30
}
2025-07-21 03:32:19 +03:30
.input-container {
2025-07-19 16:34:23 +03:30
padding: 12px 16px;
}
.message {
2025-07-21 03:32:19 +03:30
max-width: 90%;
2025-07-19 16:34:23 +03:30
}
2025-07-19 13:49:33 +03:30
}
.conversation-item {
cursor: pointer;
border-radius: 12px;
transition: background 0.2s;
}
.conversation-item:hover {
background: #f5f5f5;
}
.conv-dialog-card {
border-radius: 20px;
box-shadow: 0 8px 32px rgba(25, 118, 210, 0.10);
background: linear-gradient(135deg, #f8fafc 0%, #e3eafc 100%);
}
.conv-dialog-title {
background: linear-gradient(90deg, #1976d2 0%, #42a5f5 100%);
color: white;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
min-height: 56px;
}
.conv-new-btn {
border-radius: 12px;
font-weight: bold;
font-size: 15px;
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.08);
}
.conv-list {
background: transparent;
}
.conv-list-item {
margin-bottom: 6px;
border-radius: 14px;
transition: background 0.2s, box-shadow 0.2s;
box-shadow: 0 1px 4px rgba(25, 118, 210, 0.04);
border: 1px solid #e3eafc;
}
.conv-list-item:hover, .active-conv {
background: linear-gradient(90deg, #e3f2fd 0%, #f5faff 100%);
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.10);
border-color: #90caf9;
}
.empty-state {
opacity: 0.7;
font-size: 15px;
}
.conv-dialog-card-new {
border-radius: 24px;
box-shadow: 0 12px 40px rgba(25, 118, 210, 0.13);
background: linear-gradient(135deg, #fafdff 0%, #e3eafc 100%);
overflow: hidden;
}
.conv-dialog-header-new {
background: linear-gradient(90deg, #1976d2 0%, #42a5f5 100%);
color: white;
border-top-left-radius: 24px;
border-top-right-radius: 24px;
min-height: 64px;
font-size: 20px;
letter-spacing: 0.5px;
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.08);
}
.conv-dialog-body-new {
padding: 18px 0 0 0;
min-height: 320px;
max-height: 60vh;
overflow-y: auto;
}
.conv-card-item-new {
display: flex;
align-items: center;
background: linear-gradient(90deg, #f5faff 0%, #e3f2fd 100%);
border-radius: 18px;
box-shadow: 0 2px 12px rgba(25, 118, 210, 0.07);
margin-bottom: 14px;
padding: 12px 18px 12px 8px;
transition: box-shadow 0.2s, background 0.2s;
border: 1.5px solid #e3eafc;
cursor: pointer;
position: relative;
}
.conv-card-item-new.active {
background: linear-gradient(90deg, #e3f2fd 0%, #bbdefb 100%);
border-color: #90caf9;
box-shadow: 0 6px 24px rgba(25, 118, 210, 0.13);
}
.conv-card-main {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.conv-title-row {
display: flex;
align-items: center;
gap: 10px;
font-size: 16px;
font-weight: 600;
}
.conv-title {
color: #1976d2;
font-weight: bold;
font-size: 16px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conv-count {
color: #42a5f5;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 2px;
}
.conv-last-message {
color: #607d8b;
font-size: 13px;
margin-top: 2px;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conv-time {
color: #90a4ae;
font-size: 12px;
margin-right: 12px;
min-width: 70px;
text-align: left;
}
.conv-delete-btn {
margin-right: 8px;
margin-left: 0;
z-index: 2;
}
.conv-dialog-actions-new {
padding: 18px 24px 18px 24px;
background: transparent;
border-bottom-left-radius: 24px;
border-bottom-right-radius: 24px;
box-shadow: 0 -2px 8px rgba(25, 118, 210, 0.04);
}
.conv-new-btn-new {
border-radius: 14px;
font-weight: bold;
font-size: 16px;
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.10);
padding: 14px 0;
}
.conv-empty-state-new {
opacity: 0.8;
font-size: 16px;
text-align: center;
margin-top: 48px;
}
.conv-list-fade-enter-active, .conv-list-fade-leave-active {
transition: all 0.3s cubic-bezier(.4,0,.2,1);
}
.conv-list-fade-enter-from, .conv-list-fade-leave-to {
opacity: 0;
transform: translateY(20px);
}
.conv-list-item-v {
border-radius: 14px;
margin-bottom: 4px;
transition: background 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.conv-list-item-v:hover, .conv-list-item-v.v-list-item--active {
background: linear-gradient(90deg, #e3f2fd 0%, #f5faff 100%);
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.10);
}
.conv-preview-table {
font-size: 12px;
margin-top: 2px;
background: #f8fafc;
border-radius: 6px;
overflow: hidden;
}
2025-04-12 18:50:34 +03:30
</style>