633 lines
17 KiB
Vue
633 lines
17 KiB
Vue
<script setup lang="ts">
|
|
import { Head, router } from '@inertiajs/vue3';
|
|
import { ref, computed, onMounted, nextTick } from 'vue';
|
|
import UserLayout from '@/layouts/admin/CasinoAdminLayout.vue';
|
|
import { usePage } from '@inertiajs/vue3';
|
|
|
|
const props = defineProps<{ enabled: boolean; threads: any[]; ollama?: { host: string; model: string; healthy: boolean; error?: string | null } }>();
|
|
|
|
const page = usePage();
|
|
const user = computed(() => (page.props.auth as any).user || {});
|
|
|
|
const enabled = ref<boolean>(props.enabled);
|
|
|
|
function saveEnable() {
|
|
router.post('/admin/support/settings', { enabled: enabled.value }, { preserveScroll: true });
|
|
}
|
|
|
|
function sendReply(tid: string, textRef: any) {
|
|
const text = (textRef.value || '').trim();
|
|
if (!text) return;
|
|
router.post(`/admin/support/threads/${tid}/message`, { text }, { preserveScroll: true, onFinish: () => { textRef.value = ''; } });
|
|
}
|
|
|
|
function formatTime(isoString: string) {
|
|
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
const statusMap = {
|
|
new: { text: 'NEU', color: '#888', glow: 'rgba(136,136,136,0.4)' },
|
|
ai: { text: 'KI AKTIV', color: '#00f2ff', glow: 'rgba(0,242,255,0.4)' },
|
|
stopped: { text: 'KI GESTOPPT', color: '#ffd700', glow: 'rgba(255,215,0,0.4)' },
|
|
handoff: { text: 'ÜBERGABE', color: '#ff9f43', glow: 'rgba(255,159,67,0.4)' },
|
|
agent: { text: 'AGENT', color: '#ff007a', glow: 'rgba(255,0,122,0.4)' },
|
|
closed: { text: 'GESCHLOSSEN', color: '#ff3e3e', glow: 'rgba(255,62,62,0.4)' },
|
|
};
|
|
|
|
const getStatus = (status: string) => statusMap[status as keyof typeof statusMap] || statusMap.new;
|
|
|
|
const getAvatarFallback = (name: string, background: string = 'random', color: string = 'fff') => {
|
|
const cleanName = name ? name.replace(/\s/g, '+') : 'User';
|
|
return `https://ui-avatars.com/api/?name=${cleanName}&background=${background}&color=${color}`;
|
|
};
|
|
|
|
// Helper to find the correct avatar property
|
|
const getUserAvatar = (u: any) => {
|
|
if (!u) return null;
|
|
// Try multiple common property names
|
|
return u.avatar || u.avatar_url || u.profile_photo_url || null;
|
|
};
|
|
|
|
onMounted(() => {
|
|
console.log('Support Threads Data:', props.threads); // Debugging output
|
|
nextTick(() => { if ((window as any).lucide) (window as any).lucide.createIcons(); });
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<UserLayout>
|
|
<Head title="Admin · Support" />
|
|
|
|
<div class="admin-support-container">
|
|
<!-- Header Section -->
|
|
<div class="page-header">
|
|
<div class="header-content">
|
|
<h1 class="page-title">SUPPORT <span class="highlight">DASHBOARD</span></h1>
|
|
<p class="page-subtitle">VERWALTUNG & LIVE-CHAT ÜBERSICHT</p>
|
|
</div>
|
|
<div class="header-actions">
|
|
<!-- Global Toggle -->
|
|
<div class="status-pill" @click="enabled = !enabled; saveEnable();">
|
|
<div class="sp-label">CHAT SYSTEM</div>
|
|
<div class="sp-indicator" :class="{ active: enabled }">
|
|
<div class="sp-dot"></div>
|
|
<span>{{ enabled ? 'AKTIV' : 'INAKTIV' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats / Info Grid -->
|
|
<div class="stats-grid">
|
|
<!-- Ollama Card -->
|
|
<div class="stat-card">
|
|
<div class="sc-icon">
|
|
<i data-lucide="bot"></i>
|
|
</div>
|
|
<div class="sc-info">
|
|
<div class="sc-label">KI STATUS (OLLAMA)</div>
|
|
<div class="sc-value" :class="props.ollama?.healthy ? 'text-cyan' : 'text-red'">
|
|
{{ props.ollama?.healthy ? 'ONLINE' : 'OFFLINE' }}
|
|
</div>
|
|
<div class="sc-sub">{{ props.ollama?.model || 'Kein Modell' }}</div>
|
|
</div>
|
|
<div class="sc-glow" :class="props.ollama?.healthy ? 'glow-cyan' : 'glow-red'"></div>
|
|
</div>
|
|
|
|
<!-- Active Threads Card -->
|
|
<div class="stat-card">
|
|
<div class="sc-icon">
|
|
<i data-lucide="message-square"></i>
|
|
</div>
|
|
<div class="sc-info">
|
|
<div class="sc-label">AKTIVE CHATS</div>
|
|
<div class="sc-value text-magenta">{{ props.threads?.length || 0 }}</div>
|
|
<div class="sc-sub">OFFENE ANFRAGEN</div>
|
|
</div>
|
|
<div class="sc-glow glow-magenta"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Threads List -->
|
|
<div class="threads-section">
|
|
<h2 class="section-title"><i data-lucide="layers"></i> LAUFENDE UNTERHALTUNGEN</h2>
|
|
|
|
<div v-if="!props.threads || props.threads.length === 0" class="empty-state">
|
|
<div class="es-icon"><i data-lucide="inbox"></i></div>
|
|
<h3>Keine aktiven Support-Anfragen</h3>
|
|
<p>Alles ruhig! Warte auf neue Nachrichten von Benutzern.</p>
|
|
</div>
|
|
|
|
<div class="threads-grid">
|
|
<div v-for="t in props.threads" :key="t.id" class="thread-card" :class="{ 'handoff-mode': t.status === 'handoff' }">
|
|
<!-- Thread Header -->
|
|
<div class="tc-header">
|
|
<div class="user-profile">
|
|
<div class="up-avatar">
|
|
<img
|
|
:src="getUserAvatar(t.user) || getAvatarFallback(t.user?.username)"
|
|
:alt="t.user?.username"
|
|
:onerror="`this.onerror=null; this.src='${getAvatarFallback(t.user?.username)}'`"
|
|
/>
|
|
</div>
|
|
<div class="up-info">
|
|
<div class="up-name">{{ t.user?.username }}</div>
|
|
<div class="up-id">ID: {{ t.user?.id }} · {{ t.user?.email }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="tc-status" :style="{ color: getStatus(t.status).color, borderColor: getStatus(t.status).color, boxShadow: `0 0 10px ${getStatus(t.status).glow}` }">
|
|
{{ getStatus(t.status).text }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Topic Badge -->
|
|
<div class="tc-topic" v-if="t.topic">
|
|
<i data-lucide="hash"></i> {{ t.topic }}
|
|
</div>
|
|
|
|
<!-- Chat Area -->
|
|
<div class="tc-chat-window">
|
|
<div v-for="m in t.messages" :key="m.id" class="chat-row" :class="m.sender === 'agent' ? 'right' : 'left'">
|
|
|
|
<!-- Avatar Left (User/AI) -->
|
|
<div class="chat-avatar" v-if="m.sender !== 'agent' && m.sender !== 'system'">
|
|
<img v-if="m.sender === 'user'"
|
|
:src="getUserAvatar(t.user) || getAvatarFallback(t.user?.username)"
|
|
:onerror="`this.onerror=null; this.src='${getAvatarFallback(t.user?.username)}'`"
|
|
/>
|
|
<img v-else-if="m.sender === 'ai'" :src="getAvatarFallback('AI', '00f2ff', '000')" />
|
|
</div>
|
|
|
|
<div class="msg-bubble" :class="m.sender">
|
|
<div class="msg-text">{{ m.body }}</div>
|
|
<div class="msg-meta">
|
|
<span>{{ formatTime(m.at) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Avatar Right (Agent) -->
|
|
<div class="chat-avatar" v-if="m.sender === 'agent'">
|
|
<img
|
|
:src="getUserAvatar(user) || getAvatarFallback(user.name, 'ff007a')"
|
|
:onerror="`this.onerror=null; this.src='${getAvatarFallback(user.name, 'ff007a')}'`"
|
|
/>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="tc-actions">
|
|
<div class="input-group">
|
|
<input
|
|
type="text"
|
|
:ref="el => t._ref = el"
|
|
:disabled="t.status==='closed'"
|
|
placeholder="Antwort schreiben..."
|
|
@keydown.enter="sendReply(t.id, t._ref)"
|
|
/>
|
|
<button class="btn-send" :disabled="t.status==='closed'" @click="sendReply(t.id, t._ref)">
|
|
<i data-lucide="send"></i>
|
|
</button>
|
|
</div>
|
|
<button class="btn-close" @click="router.post(`/admin/support/threads/${t.id}/close`, {}, { preserveScroll: true })" title="Chat schließen">
|
|
<i data-lucide="x-circle"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</UserLayout>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* --- Variables & Base --- */
|
|
.admin-support-container {
|
|
padding: 30px;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
color: #fff;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
/* --- Header --- */
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-end;
|
|
margin-bottom: 40px;
|
|
border-bottom: 1px solid #1f1f1f;
|
|
padding-bottom: 20px;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 32px;
|
|
font-weight: 900;
|
|
letter-spacing: -1px;
|
|
margin: 0;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.highlight {
|
|
color: #ff007a;
|
|
text-shadow: 0 0 20px rgba(255, 0, 122, 0.4);
|
|
}
|
|
|
|
.page-subtitle {
|
|
color: #888;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: 1px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
/* --- Status Pill (Toggle) --- */
|
|
.status-pill {
|
|
background: #0f0f0f;
|
|
border: 1px solid #222;
|
|
border-radius: 12px;
|
|
padding: 6px 8px 6px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
cursor: pointer;
|
|
transition: 0.3s;
|
|
}
|
|
.status-pill:hover {
|
|
border-color: #333;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.sp-label {
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
color: #666;
|
|
}
|
|
|
|
.sp-indicator {
|
|
background: #1a1a1a;
|
|
padding: 6px 12px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
color: #555;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
.sp-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #444;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
.sp-indicator.active {
|
|
background: rgba(0, 242, 255, 0.1);
|
|
color: #00f2ff;
|
|
}
|
|
.sp-indicator.active .sp-dot {
|
|
background: #00f2ff;
|
|
box-shadow: 0 0 8px #00f2ff;
|
|
}
|
|
|
|
/* --- Stats Grid --- */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: #0f0f0f;
|
|
border: 1px solid #1f1f1f;
|
|
border-radius: 16px;
|
|
padding: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: 0.3s;
|
|
}
|
|
.stat-card:hover {
|
|
transform: translateY(-3px);
|
|
border-color: #333;
|
|
}
|
|
|
|
.sc-icon {
|
|
width: 50px;
|
|
height: 50px;
|
|
background: #141414;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #666;
|
|
z-index: 2;
|
|
}
|
|
.sc-icon i { width: 24px; height: 24px; }
|
|
|
|
.sc-info { z-index: 2; }
|
|
.sc-label { font-size: 10px; font-weight: 800; color: #555; letter-spacing: 1px; margin-bottom: 4px; }
|
|
.sc-value { font-size: 20px; font-weight: 900; letter-spacing: -0.5px; }
|
|
.sc-sub { font-size: 11px; color: #666; margin-top: 2px; font-family: monospace; }
|
|
|
|
.text-cyan { color: #00f2ff; text-shadow: 0 0 10px rgba(0,242,255,0.3); }
|
|
.text-red { color: #ff3e3e; text-shadow: 0 0 10px rgba(255,62,62,0.3); }
|
|
.text-magenta { color: #ff007a; text-shadow: 0 0 10px rgba(255,0,122,0.3); }
|
|
|
|
.sc-glow {
|
|
position: absolute;
|
|
top: -50%;
|
|
right: -20%;
|
|
width: 200px;
|
|
height: 200px;
|
|
border-radius: 50%;
|
|
filter: blur(60px);
|
|
opacity: 0.15;
|
|
z-index: 1;
|
|
}
|
|
.glow-cyan { background: #00f2ff; }
|
|
.glow-red { background: #ff3e3e; }
|
|
.glow-magenta { background: #ff007a; }
|
|
|
|
/* --- Threads Section --- */
|
|
.section-title {
|
|
font-size: 14px;
|
|
font-weight: 800;
|
|
color: #888;
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
letter-spacing: 1px;
|
|
}
|
|
.section-title i { width: 16px; }
|
|
|
|
.empty-state {
|
|
background: #0f0f0f;
|
|
border: 1px solid #1f1f1f;
|
|
border-radius: 16px;
|
|
padding: 60px;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
.es-icon {
|
|
width: 60px;
|
|
height: 60px;
|
|
background: #141414;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #333;
|
|
margin-bottom: 20px;
|
|
}
|
|
.es-icon i { width: 28px; height: 28px; }
|
|
.empty-state h3 { font-size: 18px; font-weight: 700; margin: 0 0 8px 0; color: #fff; }
|
|
.empty-state p { font-size: 13px; color: #666; margin: 0; }
|
|
|
|
.threads-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
/* --- Thread Card --- */
|
|
.thread-card {
|
|
background: #0f0f0f;
|
|
border: 1px solid #1f1f1f;
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
transition: 0.3s;
|
|
height: 600px; /* Taller for better chat view */
|
|
}
|
|
.thread-card:hover {
|
|
border-color: #333;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
}
|
|
.thread-card.handoff-mode {
|
|
border-color: #ff9f43;
|
|
box-shadow: 0 0 20px rgba(255, 159, 67, 0.15);
|
|
}
|
|
|
|
.tc-header {
|
|
padding: 16px;
|
|
background: #141414;
|
|
border-bottom: 1px solid #1f1f1f;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.user-profile { display: flex; align-items: center; gap: 12px; }
|
|
.up-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
overflow: hidden;
|
|
border: 2px solid #222;
|
|
}
|
|
.up-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
.up-info { display: flex; flex-direction: column; }
|
|
.up-name { font-size: 14px; font-weight: 800; color: #fff; }
|
|
.up-id { font-size: 10px; color: #666; font-family: monospace; }
|
|
|
|
.tc-status {
|
|
font-size: 9px;
|
|
font-weight: 900;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
border: 1px solid;
|
|
background: rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.tc-topic {
|
|
padding: 8px 16px;
|
|
background: #111;
|
|
border-bottom: 1px solid #1f1f1f;
|
|
font-size: 11px;
|
|
color: #888;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.tc-topic i { width: 12px; }
|
|
|
|
/* --- Chat Window --- */
|
|
.tc-chat-window {
|
|
flex: 1;
|
|
background: #0a0a0a;
|
|
padding: 16px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.chat-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: flex-end;
|
|
max-width: 85%;
|
|
}
|
|
|
|
.chat-row.left { align-self: flex-start; }
|
|
.chat-row.right { align-self: flex-end; flex-direction: row-reverse; }
|
|
|
|
.chat-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
border: 1px solid #333;
|
|
}
|
|
.chat-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
|
|
.msg-bubble {
|
|
padding: 12px 16px;
|
|
border-radius: 16px;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
position: relative;
|
|
word-break: break-word;
|
|
}
|
|
|
|
/* User Bubble */
|
|
.user {
|
|
background: #1a1a1a;
|
|
color: #ddd;
|
|
border-bottom-left-radius: 2px;
|
|
border: 1px solid #222;
|
|
}
|
|
|
|
/* Agent Bubble */
|
|
.agent {
|
|
background: rgba(255, 0, 122, 0.1);
|
|
color: #fff;
|
|
border-bottom-right-radius: 2px;
|
|
border: 1px solid rgba(255, 0, 122, 0.3);
|
|
}
|
|
|
|
/* AI Bubble */
|
|
.ai {
|
|
background: rgba(0, 242, 255, 0.05);
|
|
color: #00f2ff;
|
|
border-bottom-left-radius: 2px;
|
|
border: 1px solid rgba(0, 242, 255, 0.2);
|
|
}
|
|
|
|
/* System Bubble */
|
|
.system {
|
|
background: transparent;
|
|
color: #666;
|
|
font-size: 11px;
|
|
font-style: italic;
|
|
padding: 4px;
|
|
border: none;
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
.chat-row:has(.system) { max-width: 100%; align-self: center; }
|
|
|
|
.msg-meta {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 8px;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
opacity: 0.5;
|
|
gap: 6px;
|
|
}
|
|
|
|
/* --- Actions --- */
|
|
.tc-actions {
|
|
padding: 12px;
|
|
background: #141414;
|
|
border-top: 1px solid #1f1f1f;
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.input-group {
|
|
flex: 1;
|
|
display: flex;
|
|
background: #0a0a0a;
|
|
border: 1px solid #222;
|
|
border-radius: 10px;
|
|
padding: 4px;
|
|
transition: 0.2s;
|
|
}
|
|
.input-group:focus-within {
|
|
border-color: #ff007a;
|
|
box-shadow: 0 0 10px rgba(255,0,122,0.1);
|
|
}
|
|
|
|
.input-group input {
|
|
flex: 1;
|
|
background: transparent;
|
|
border: none;
|
|
color: #fff;
|
|
padding: 0 10px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.btn-send {
|
|
width: 32px;
|
|
height: 32px;
|
|
background: #ff007a;
|
|
border: none;
|
|
border-radius: 8px;
|
|
color: #fff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
}
|
|
.btn-send:hover { background: #d40065; }
|
|
.btn-send:disabled { background: #333; cursor: not-allowed; }
|
|
.btn-send i { width: 16px; }
|
|
|
|
.btn-close {
|
|
width: 42px;
|
|
background: rgba(255, 62, 62, 0.1);
|
|
border: 1px solid rgba(255, 62, 62, 0.2);
|
|
border-radius: 10px;
|
|
color: #ff3e3e;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
}
|
|
.btn-close:hover {
|
|
background: rgba(255, 62, 62, 0.2);
|
|
border-color: #ff3e3e;
|
|
}
|
|
.btn-close i { width: 20px; }
|
|
|
|
/* Scrollbar for chat */
|
|
.tc-chat-window::-webkit-scrollbar { width: 4px; }
|
|
.tc-chat-window::-webkit-scrollbar-track { background: transparent; }
|
|
.tc-chat-window::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
|
|
|
|
@media (max-width: 768px) {
|
|
.page-header { flex-direction: column; align-items: flex-start; gap: 20px; }
|
|
.threads-grid { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|