agent-consultation-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/api/index.d.ts +27 -0
- package/dist/api/index.js +213 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/middleware/security.d.ts +6 -0
- package/dist/api/middleware/security.js +28 -0
- package/dist/api/middleware/security.js.map +1 -0
- package/dist/api/routes/chat.d.ts +2 -0
- package/dist/api/routes/chat.js +61 -0
- package/dist/api/routes/chat.js.map +1 -0
- package/dist/api/routes/config.d.ts +2 -0
- package/dist/api/routes/config.js +81 -0
- package/dist/api/routes/config.js.map +1 -0
- package/dist/api/routes/providers.d.ts +2 -0
- package/dist/api/routes/providers.js +225 -0
- package/dist/api/routes/providers.js.map +1 -0
- package/dist/api/standalone-server.d.ts +12 -0
- package/dist/api/standalone-server.js +91 -0
- package/dist/api/standalone-server.js.map +1 -0
- package/dist/config/defaults.d.ts +17 -0
- package/dist/config/defaults.js +30 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/encryption.d.ts +12 -0
- package/dist/config/encryption.js +85 -0
- package/dist/config/encryption.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/manager.d.ts +62 -0
- package/dist/config/manager.js +186 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/prompts.d.ts +10 -0
- package/dist/config/prompts.js +140 -0
- package/dist/config/prompts.js.map +1 -0
- package/dist/config/schema.d.ts +141 -0
- package/dist/config/schema.js +54 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +224 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/base.d.ts +44 -0
- package/dist/providers/base.js +84 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/deepseek.d.ts +30 -0
- package/dist/providers/deepseek.js +148 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai.d.ts +30 -0
- package/dist/providers/openai.js +123 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/registry.d.ts +37 -0
- package/dist/providers/registry.js +65 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +71 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/server/conversation.d.ts +101 -0
- package/dist/server/conversation.js +275 -0
- package/dist/server/conversation.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/tools/consult.d.ts +15 -0
- package/dist/server/tools/consult.js +164 -0
- package/dist/server/tools/consult.js.map +1 -0
- package/dist/server/tools/index.d.ts +1 -0
- package/dist/server/tools/index.js +2 -0
- package/dist/server/tools/index.js.map +1 -0
- package/dist/types/config.d.ts +20 -0
- package/dist/types/config.js +2 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/messages.d.ts +48 -0
- package/dist/types/messages.js +2 -0
- package/dist/types/messages.js.map +1 -0
- package/dist/types/models.d.ts +39 -0
- package/dist/types/models.js +66 -0
- package/dist/types/models.js.map +1 -0
- package/dist/ui/index.html +1044 -0
- package/dist/utils/errors.d.ts +32 -0
- package/dist/utils/errors.js +53 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.js +73 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Agent Consultation MCP - Configuration</title>
|
|
7
|
+
|
|
8
|
+
<!-- Geist Font - Vercel's distinctive typeface -->
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
12
|
+
|
|
13
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
14
|
+
<script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
|
15
|
+
<script>
|
|
16
|
+
tailwind.config = {
|
|
17
|
+
darkMode: 'class',
|
|
18
|
+
theme: {
|
|
19
|
+
extend: {
|
|
20
|
+
fontFamily: {
|
|
21
|
+
sans: ['Geist', 'system-ui', 'sans-serif'],
|
|
22
|
+
mono: ['Geist Mono', 'monospace'],
|
|
23
|
+
},
|
|
24
|
+
colors: {
|
|
25
|
+
// Custom dark theme with amber/orange accent
|
|
26
|
+
surface: {
|
|
27
|
+
DEFAULT: '#0c0c0c',
|
|
28
|
+
50: '#171717',
|
|
29
|
+
100: '#1a1a1a',
|
|
30
|
+
200: '#262626',
|
|
31
|
+
300: '#404040',
|
|
32
|
+
},
|
|
33
|
+
accent: {
|
|
34
|
+
DEFAULT: '#f59e0b',
|
|
35
|
+
50: '#fffbeb',
|
|
36
|
+
100: '#fef3c7',
|
|
37
|
+
200: '#fde68a',
|
|
38
|
+
300: '#fcd34d',
|
|
39
|
+
400: '#fbbf24',
|
|
40
|
+
500: '#f59e0b',
|
|
41
|
+
600: '#d97706',
|
|
42
|
+
700: '#b45309',
|
|
43
|
+
800: '#92400e',
|
|
44
|
+
900: '#78350f',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
animation: {
|
|
48
|
+
'fade-in': 'fadeIn 0.5s ease-out forwards',
|
|
49
|
+
'fade-in-up': 'fadeInUp 0.6s ease-out forwards',
|
|
50
|
+
'slide-in': 'slideIn 0.4s ease-out forwards',
|
|
51
|
+
'pulse-glow': 'pulseGlow 2s ease-in-out infinite',
|
|
52
|
+
'float': 'float 6s ease-in-out infinite',
|
|
53
|
+
},
|
|
54
|
+
keyframes: {
|
|
55
|
+
fadeIn: {
|
|
56
|
+
'0%': { opacity: '0' },
|
|
57
|
+
'100%': { opacity: '1' },
|
|
58
|
+
},
|
|
59
|
+
fadeInUp: {
|
|
60
|
+
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
|
61
|
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
|
62
|
+
},
|
|
63
|
+
slideIn: {
|
|
64
|
+
'0%': { opacity: '0', transform: 'translateX(-10px)' },
|
|
65
|
+
'100%': { opacity: '1', transform: 'translateX(0)' },
|
|
66
|
+
},
|
|
67
|
+
pulseGlow: {
|
|
68
|
+
'0%, 100%': { boxShadow: '0 0 20px rgba(245, 158, 11, 0.1)' },
|
|
69
|
+
'50%': { boxShadow: '0 0 40px rgba(245, 158, 11, 0.2)' },
|
|
70
|
+
},
|
|
71
|
+
float: {
|
|
72
|
+
'0%, 100%': { transform: 'translateY(0px)' },
|
|
73
|
+
'50%': { transform: 'translateY(-10px)' },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
<style>
|
|
81
|
+
[x-cloak] { display: none !important; }
|
|
82
|
+
|
|
83
|
+
/* Noise texture overlay */
|
|
84
|
+
.noise-bg::before {
|
|
85
|
+
content: '';
|
|
86
|
+
position: fixed;
|
|
87
|
+
top: 0;
|
|
88
|
+
left: 0;
|
|
89
|
+
width: 100%;
|
|
90
|
+
height: 100%;
|
|
91
|
+
pointer-events: none;
|
|
92
|
+
opacity: 0.03;
|
|
93
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Gradient mesh background */
|
|
97
|
+
.gradient-mesh {
|
|
98
|
+
background:
|
|
99
|
+
radial-gradient(ellipse at 20% 0%, rgba(245, 158, 11, 0.08) 0%, transparent 50%),
|
|
100
|
+
radial-gradient(ellipse at 80% 100%, rgba(217, 119, 6, 0.06) 0%, transparent 50%),
|
|
101
|
+
radial-gradient(ellipse at 50% 50%, rgba(180, 83, 9, 0.04) 0%, transparent 70%);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Glassmorphism card */
|
|
105
|
+
.glass-card {
|
|
106
|
+
background: rgba(23, 23, 23, 0.6);
|
|
107
|
+
backdrop-filter: blur(12px);
|
|
108
|
+
-webkit-backdrop-filter: blur(12px);
|
|
109
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.glass-card:hover {
|
|
113
|
+
background: rgba(26, 26, 26, 0.8);
|
|
114
|
+
border-color: rgba(245, 158, 11, 0.2);
|
|
115
|
+
box-shadow: 0 0 30px rgba(245, 158, 11, 0.08);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Staggered animation delays */
|
|
119
|
+
.stagger-1 { animation-delay: 0.1s; }
|
|
120
|
+
.stagger-2 { animation-delay: 0.2s; }
|
|
121
|
+
.stagger-3 { animation-delay: 0.3s; }
|
|
122
|
+
.stagger-4 { animation-delay: 0.4s; }
|
|
123
|
+
|
|
124
|
+
/* Custom scrollbar */
|
|
125
|
+
::-webkit-scrollbar {
|
|
126
|
+
width: 8px;
|
|
127
|
+
}
|
|
128
|
+
::-webkit-scrollbar-track {
|
|
129
|
+
background: #0c0c0c;
|
|
130
|
+
}
|
|
131
|
+
::-webkit-scrollbar-thumb {
|
|
132
|
+
background: #404040;
|
|
133
|
+
border-radius: 4px;
|
|
134
|
+
}
|
|
135
|
+
::-webkit-scrollbar-thumb:hover {
|
|
136
|
+
background: #525252;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Input focus glow */
|
|
140
|
+
input:focus, select:focus {
|
|
141
|
+
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Button hover glow */
|
|
145
|
+
.btn-accent:hover {
|
|
146
|
+
box-shadow: 0 0 20px rgba(245, 158, 11, 0.3);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Line clamp for card preview */
|
|
150
|
+
.line-clamp-3 {
|
|
151
|
+
display: -webkit-box;
|
|
152
|
+
-webkit-line-clamp: 3;
|
|
153
|
+
-webkit-box-orient: vertical;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Card relative positioning for hover effect */
|
|
158
|
+
.glass-card {
|
|
159
|
+
position: relative;
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
162
|
+
</head>
|
|
163
|
+
<body class="bg-surface text-neutral-100 min-h-screen font-sans noise-bg gradient-mesh">
|
|
164
|
+
<div x-data="configApp()" x-init="init()" x-cloak class="max-w-5xl mx-auto px-6 py-12">
|
|
165
|
+
|
|
166
|
+
<!-- Header -->
|
|
167
|
+
<header class="mb-16 opacity-0 animate-fade-in-up">
|
|
168
|
+
<div class="flex items-center justify-center mb-4">
|
|
169
|
+
<div class="relative">
|
|
170
|
+
<!-- Animated glow ring -->
|
|
171
|
+
<div class="absolute inset-0 bg-accent-500/20 rounded-full blur-xl animate-pulse-glow"></div>
|
|
172
|
+
<div class="relative w-16 h-16 bg-gradient-to-br from-accent-400 to-accent-600 rounded-2xl flex items-center justify-center transform rotate-12 hover:rotate-0 transition-transform duration-500">
|
|
173
|
+
<svg class="w-8 h-8 text-surface" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
174
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
175
|
+
</svg>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<h1 class="text-4xl md:text-5xl font-bold text-center tracking-tight">
|
|
180
|
+
<span class="bg-gradient-to-r from-neutral-100 via-neutral-200 to-neutral-400 bg-clip-text text-transparent">
|
|
181
|
+
Agent Consultation
|
|
182
|
+
</span>
|
|
183
|
+
<span class="text-accent-500"> MCP</span>
|
|
184
|
+
</h1>
|
|
185
|
+
<p class="text-neutral-500 text-center mt-3 text-lg">Configure your AI provider API keys</p>
|
|
186
|
+
</header>
|
|
187
|
+
|
|
188
|
+
<!-- Toast Notifications (z-[60] to appear above modal z-50) -->
|
|
189
|
+
<div class="fixed top-6 right-6 z-[60] space-y-3">
|
|
190
|
+
<template x-for="toast in toasts" :key="toast.id">
|
|
191
|
+
<div
|
|
192
|
+
x-show="toast.show"
|
|
193
|
+
x-transition:enter="transition ease-out duration-300"
|
|
194
|
+
x-transition:enter-start="opacity-0 transform translate-x-8 scale-95"
|
|
195
|
+
x-transition:enter-end="opacity-100 transform translate-x-0 scale-100"
|
|
196
|
+
x-transition:leave="transition ease-in duration-200"
|
|
197
|
+
x-transition:leave-start="opacity-100 scale-100"
|
|
198
|
+
x-transition:leave-end="opacity-0 scale-95"
|
|
199
|
+
:class="{
|
|
200
|
+
'bg-emerald-500/20 border-emerald-500/30 text-emerald-300': toast.type === 'success',
|
|
201
|
+
'bg-red-500/20 border-red-500/30 text-red-300': toast.type === 'error',
|
|
202
|
+
'bg-accent-500/20 border-accent-500/30 text-accent-300': toast.type === 'info'
|
|
203
|
+
}"
|
|
204
|
+
class="px-5 py-3 rounded-xl border backdrop-blur-md shadow-2xl flex items-center space-x-3 font-medium"
|
|
205
|
+
>
|
|
206
|
+
<template x-if="toast.type === 'success'">
|
|
207
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
208
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
209
|
+
</svg>
|
|
210
|
+
</template>
|
|
211
|
+
<template x-if="toast.type === 'error'">
|
|
212
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
213
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
214
|
+
</svg>
|
|
215
|
+
</template>
|
|
216
|
+
<template x-if="toast.type === 'info'">
|
|
217
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
218
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
219
|
+
</svg>
|
|
220
|
+
</template>
|
|
221
|
+
<span x-text="toast.message"></span>
|
|
222
|
+
</div>
|
|
223
|
+
</template>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Loading State -->
|
|
227
|
+
<div x-show="loading" class="flex flex-col items-center justify-center py-32">
|
|
228
|
+
<div class="relative">
|
|
229
|
+
<div class="absolute inset-0 bg-accent-500/20 rounded-full blur-2xl animate-pulse"></div>
|
|
230
|
+
<div class="relative w-16 h-16 border-4 border-surface-200 border-t-accent-500 rounded-full animate-spin"></div>
|
|
231
|
+
</div>
|
|
232
|
+
<p class="mt-6 text-neutral-500 font-medium">Loading configuration...</p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<!-- Error State -->
|
|
236
|
+
<div x-show="!loading && loadError" class="flex flex-col items-center justify-center py-32 opacity-0 animate-fade-in-up">
|
|
237
|
+
<div class="relative">
|
|
238
|
+
<div class="absolute inset-0 bg-red-500/10 rounded-full blur-2xl"></div>
|
|
239
|
+
<div class="relative w-20 h-20 bg-red-500/10 rounded-2xl flex items-center justify-center border border-red-500/20">
|
|
240
|
+
<svg class="w-10 h-10 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
241
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
|
242
|
+
</svg>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
<h3 class="text-2xl font-semibold text-neutral-100 mt-6">Connection Failed</h3>
|
|
246
|
+
<p class="text-neutral-500 mt-2 text-center max-w-md" x-text="loadError"></p>
|
|
247
|
+
<button
|
|
248
|
+
@click="loading = true; loadError = null; init()"
|
|
249
|
+
class="mt-6 px-6 py-3 bg-accent-600 hover:bg-accent-500 rounded-xl font-semibold transition-all duration-300 btn-accent"
|
|
250
|
+
>
|
|
251
|
+
Try Again
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- Main Content -->
|
|
256
|
+
<div x-show="!loading && !loadError" class="space-y-12">
|
|
257
|
+
|
|
258
|
+
<!-- Providers Section -->
|
|
259
|
+
<section class="opacity-0 animate-fade-in-up stagger-1">
|
|
260
|
+
<div class="flex items-center mb-6">
|
|
261
|
+
<h2 class="text-2xl font-semibold flex items-center">
|
|
262
|
+
<div class="w-10 h-10 bg-accent-500/10 rounded-xl flex items-center justify-center mr-3">
|
|
263
|
+
<svg class="w-5 h-5 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
264
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
265
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
266
|
+
</svg>
|
|
267
|
+
</div>
|
|
268
|
+
API Providers
|
|
269
|
+
</h2>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<!-- Provider Cards Grid -->
|
|
273
|
+
<div class="grid gap-4 md:grid-cols-2 mb-8">
|
|
274
|
+
<template x-for="provider in providers" :key="provider.id">
|
|
275
|
+
<div class="glass-card rounded-2xl p-5 transition-all duration-300">
|
|
276
|
+
<!-- Provider Header -->
|
|
277
|
+
<div class="flex items-center justify-between mb-4">
|
|
278
|
+
<div class="flex items-center">
|
|
279
|
+
<div
|
|
280
|
+
class="w-10 h-10 rounded-xl flex items-center justify-center mr-3 border"
|
|
281
|
+
:class="provider.id === 'deepseek'
|
|
282
|
+
? 'bg-gradient-to-br from-blue-500/20 to-purple-500/20 border-blue-500/20'
|
|
283
|
+
: 'bg-gradient-to-br from-emerald-500/20 to-teal-500/20 border-emerald-500/20'"
|
|
284
|
+
>
|
|
285
|
+
<svg class="w-5 h-5" :class="provider.id === 'deepseek' ? 'text-blue-400' : 'text-emerald-400'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
286
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
287
|
+
</svg>
|
|
288
|
+
</div>
|
|
289
|
+
<div>
|
|
290
|
+
<h3 class="text-base font-semibold text-neutral-100" x-text="provider.name"></h3>
|
|
291
|
+
<p class="text-xs text-neutral-500" x-text="provider.description"></p>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
<div
|
|
295
|
+
class="flex items-center px-2 py-1 rounded-lg"
|
|
296
|
+
:class="provider.hasKey ? 'bg-emerald-500/10 border border-emerald-500/20' : 'bg-red-500/10 border border-red-500/20'"
|
|
297
|
+
>
|
|
298
|
+
<div class="w-1.5 h-1.5 rounded-full mr-1.5" :class="provider.hasKey ? 'bg-emerald-400' : 'bg-red-400'"></div>
|
|
299
|
+
<span class="text-[10px] font-medium" :class="provider.hasKey ? 'text-emerald-400' : 'text-red-400'" x-text="provider.hasKey ? 'Connected' : 'Not Set'"></span>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<!-- API Key Status -->
|
|
304
|
+
<template x-if="provider.hasKey">
|
|
305
|
+
<div class="flex items-center text-xs bg-surface-50/50 rounded-lg px-3 py-2 mb-4">
|
|
306
|
+
<svg class="w-3 h-3 text-emerald-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
307
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
|
308
|
+
</svg>
|
|
309
|
+
<code class="text-emerald-400 font-mono text-[11px]" x-text="provider.maskedKey"></code>
|
|
310
|
+
</div>
|
|
311
|
+
</template>
|
|
312
|
+
|
|
313
|
+
<!-- Models List -->
|
|
314
|
+
<div class="flex flex-wrap gap-1.5 mb-4">
|
|
315
|
+
<template x-for="model in provider.models" :key="model.id">
|
|
316
|
+
<span class="text-[10px] px-2 py-1 bg-surface-200/50 text-neutral-400 rounded font-mono" x-text="model.name"></span>
|
|
317
|
+
</template>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<!-- Action Buttons -->
|
|
321
|
+
<div class="flex items-center gap-2">
|
|
322
|
+
<button
|
|
323
|
+
@click="openKeyModal(provider)"
|
|
324
|
+
class="flex-1 px-3 py-2 bg-accent-600 hover:bg-accent-500 rounded-lg text-xs font-semibold transition-all duration-300 btn-accent"
|
|
325
|
+
x-text="provider.hasKey ? 'Update Key' : 'Add API Key'"
|
|
326
|
+
></button>
|
|
327
|
+
<template x-if="provider.hasKey">
|
|
328
|
+
<button
|
|
329
|
+
@click="testKey(provider.id)"
|
|
330
|
+
:disabled="testing === provider.id"
|
|
331
|
+
class="px-3 py-2 bg-surface-200 hover:bg-surface-300 rounded-lg text-xs font-medium transition-all duration-300 disabled:opacity-50"
|
|
332
|
+
>
|
|
333
|
+
<span x-show="testing !== provider.id">Test</span>
|
|
334
|
+
<svg x-show="testing === provider.id" class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24">
|
|
335
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
336
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
337
|
+
</svg>
|
|
338
|
+
</button>
|
|
339
|
+
</template>
|
|
340
|
+
<template x-if="provider.hasKey">
|
|
341
|
+
<button
|
|
342
|
+
@click="removeKey(provider.id)"
|
|
343
|
+
class="px-2 py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 rounded-lg text-xs font-medium transition-all duration-300 border border-red-500/20"
|
|
344
|
+
title="Remove API Key"
|
|
345
|
+
>
|
|
346
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
347
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
348
|
+
</svg>
|
|
349
|
+
</button>
|
|
350
|
+
</template>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</template>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<!-- Settings Section -->
|
|
357
|
+
<div class="glass-card rounded-2xl p-6">
|
|
358
|
+
<h3 class="text-lg font-semibold text-neutral-100 mb-4">Settings</h3>
|
|
359
|
+
<div class="grid gap-5 md:grid-cols-2">
|
|
360
|
+
<!-- Default Model -->
|
|
361
|
+
<div>
|
|
362
|
+
<label class="block text-xs font-medium text-neutral-500 mb-2 uppercase tracking-wider">Default Model</label>
|
|
363
|
+
<select
|
|
364
|
+
x-model="config.defaultModel"
|
|
365
|
+
@change="saveConfig()"
|
|
366
|
+
class="w-full bg-surface-100 border border-surface-300 rounded-xl px-4 py-3 text-neutral-100 focus:ring-2 focus:ring-accent-500/50 focus:border-accent-500 transition-all duration-300 font-mono text-sm appearance-none cursor-pointer"
|
|
367
|
+
style="background-image: url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 fill=%27none%27 viewBox=%270 0 24 24%27 stroke=%27%23737373%27%3E%3Cpath stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27M19 9l-7 7-7-7%27/%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: right 1rem center; background-size: 1rem;"
|
|
368
|
+
>
|
|
369
|
+
<template x-for="model in config.availableModels" :key="model">
|
|
370
|
+
<option :value="model" x-text="model"></option>
|
|
371
|
+
</template>
|
|
372
|
+
</select>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<!-- Max Messages -->
|
|
376
|
+
<div>
|
|
377
|
+
<label class="block text-xs font-medium text-neutral-500 mb-2 uppercase tracking-wider">Max Messages</label>
|
|
378
|
+
<input
|
|
379
|
+
type="number"
|
|
380
|
+
x-model="config.maxMessages"
|
|
381
|
+
@change="saveConfig()"
|
|
382
|
+
min="1"
|
|
383
|
+
max="50"
|
|
384
|
+
class="w-full bg-surface-100 border border-surface-300 rounded-xl px-4 py-3 text-neutral-100 focus:ring-2 focus:ring-accent-500/50 focus:border-accent-500 transition-all duration-300 font-mono text-sm"
|
|
385
|
+
/>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</section>
|
|
390
|
+
|
|
391
|
+
<!-- Conversation History Section -->
|
|
392
|
+
<section class="opacity-0 animate-fade-in-up stagger-2">
|
|
393
|
+
<div class="flex items-center justify-between mb-6">
|
|
394
|
+
<div class="flex items-center">
|
|
395
|
+
<div class="w-10 h-10 bg-accent-500/10 rounded-xl flex items-center justify-center mr-3">
|
|
396
|
+
<svg class="w-5 h-5 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
397
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
|
398
|
+
</svg>
|
|
399
|
+
</div>
|
|
400
|
+
<div>
|
|
401
|
+
<h2 class="text-2xl font-semibold">Conversation History</h2>
|
|
402
|
+
<div class="flex items-center gap-3 mt-1">
|
|
403
|
+
<span class="text-xs text-emerald-400" x-text="historyStats.active + ' active'"></span>
|
|
404
|
+
<span class="text-xs text-neutral-500">|</span>
|
|
405
|
+
<span class="text-xs text-neutral-400" x-text="historyStats.archived + ' archived'"></span>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
<button
|
|
410
|
+
@click="loadChatHistory()"
|
|
411
|
+
:disabled="loadingHistory"
|
|
412
|
+
class="px-4 py-2 bg-surface-200 hover:bg-surface-300 rounded-xl text-sm font-medium transition-all duration-300 disabled:opacity-50 flex items-center"
|
|
413
|
+
>
|
|
414
|
+
<svg x-show="!loadingHistory" class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
415
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
416
|
+
</svg>
|
|
417
|
+
<svg x-show="loadingHistory" class="animate-spin w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24">
|
|
418
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
419
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
420
|
+
</svg>
|
|
421
|
+
Refresh
|
|
422
|
+
</button>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<!-- Empty State -->
|
|
426
|
+
<template x-if="!loadingHistory && chatHistory.length === 0">
|
|
427
|
+
<div class="glass-card rounded-2xl p-8 text-center">
|
|
428
|
+
<div class="w-16 h-16 bg-surface-200/50 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
429
|
+
<svg class="w-8 h-8 text-neutral-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
430
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
|
431
|
+
</svg>
|
|
432
|
+
</div>
|
|
433
|
+
<h3 class="text-lg font-semibold text-neutral-300 mb-2">No Active Conversations</h3>
|
|
434
|
+
<p class="text-neutral-500 text-sm">DeepSeek conversations from Claude Code will appear here.</p>
|
|
435
|
+
</div>
|
|
436
|
+
</template>
|
|
437
|
+
|
|
438
|
+
<!-- Conversations Card Grid -->
|
|
439
|
+
<div x-show="chatHistory.length > 0">
|
|
440
|
+
<!-- Card Grid -->
|
|
441
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-6">
|
|
442
|
+
<template x-for="conv in paginatedHistory" :key="conv.id">
|
|
443
|
+
<div
|
|
444
|
+
@click="openConversationModal(conv)"
|
|
445
|
+
class="glass-card rounded-xl p-4 cursor-pointer transition-all duration-300 hover:scale-[1.02] hover:border-accent-500/40 group"
|
|
446
|
+
:class="conv.status === 'archived' ? 'opacity-80' : ''"
|
|
447
|
+
>
|
|
448
|
+
<!-- Card Header: Status + Model -->
|
|
449
|
+
<div class="flex items-center justify-between mb-3">
|
|
450
|
+
<span
|
|
451
|
+
class="px-2 py-0.5 rounded text-[10px] font-semibold uppercase tracking-wide"
|
|
452
|
+
:class="conv.status === 'active'
|
|
453
|
+
? 'bg-emerald-500/20 text-emerald-400'
|
|
454
|
+
: 'bg-neutral-500/20 text-neutral-400'"
|
|
455
|
+
x-text="conv.status === 'active' ? 'Active' : 'Archived'"
|
|
456
|
+
></span>
|
|
457
|
+
<template x-if="conv.status === 'archived' && conv.endReason">
|
|
458
|
+
<span
|
|
459
|
+
class="px-2 py-0.5 rounded text-[10px] font-medium"
|
|
460
|
+
:class="conv.endReason === 'completed'
|
|
461
|
+
? 'bg-blue-500/20 text-blue-400'
|
|
462
|
+
: 'bg-orange-500/20 text-orange-400'"
|
|
463
|
+
x-text="conv.endReason === 'completed' ? 'Done' : 'Timeout'"
|
|
464
|
+
></span>
|
|
465
|
+
</template>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- Model Badge -->
|
|
469
|
+
<div class="mb-3">
|
|
470
|
+
<span class="px-2 py-1 bg-accent-500/15 text-accent-400 rounded text-xs font-mono" x-text="conv.model"></span>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<!-- Message Preview -->
|
|
474
|
+
<div class="mb-3 min-h-[60px]">
|
|
475
|
+
<p class="text-xs text-neutral-400 line-clamp-3" x-text="conv.messages[0]?.content.substring(0, 100) + (conv.messages[0]?.content.length > 100 ? '...' : '') || 'No messages'"></p>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<!-- Card Footer: Stats -->
|
|
479
|
+
<div class="flex items-center justify-between pt-3 border-t border-surface-300/20">
|
|
480
|
+
<div class="flex items-center text-neutral-500">
|
|
481
|
+
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
482
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
|
483
|
+
</svg>
|
|
484
|
+
<span class="text-[11px]" x-text="conv.messageCount"></span>
|
|
485
|
+
</div>
|
|
486
|
+
<span class="text-[10px] text-neutral-600" x-text="new Date(conv.createdAt).toLocaleDateString()"></span>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<!-- Hover indicator -->
|
|
490
|
+
<div class="absolute inset-0 rounded-xl border-2 border-transparent group-hover:border-accent-500/20 transition-all duration-300 pointer-events-none"></div>
|
|
491
|
+
</div>
|
|
492
|
+
</template>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
<!-- Pagination Controls -->
|
|
496
|
+
<div class="flex items-center justify-between glass-card rounded-xl px-4 py-3">
|
|
497
|
+
<div class="text-sm text-neutral-500">
|
|
498
|
+
<span x-text="'Showing ' + ((currentPage - 1) * itemsPerPage + 1) + '-' + Math.min(currentPage * itemsPerPage, chatHistory.length) + ' of ' + chatHistory.length"></span>
|
|
499
|
+
</div>
|
|
500
|
+
<div class="flex items-center gap-2">
|
|
501
|
+
<!-- Previous Button -->
|
|
502
|
+
<button
|
|
503
|
+
@click="prevPage()"
|
|
504
|
+
:disabled="currentPage === 1"
|
|
505
|
+
class="px-3 py-1.5 bg-surface-200 hover:bg-surface-300 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
506
|
+
>
|
|
507
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
508
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
509
|
+
</svg>
|
|
510
|
+
</button>
|
|
511
|
+
|
|
512
|
+
<!-- Page Numbers -->
|
|
513
|
+
<div class="flex items-center gap-1">
|
|
514
|
+
<template x-for="page in totalPages" :key="page">
|
|
515
|
+
<button
|
|
516
|
+
x-show="page === 1 || page === totalPages || (page >= currentPage - 1 && page <= currentPage + 1)"
|
|
517
|
+
@click="goToPage(page)"
|
|
518
|
+
class="w-8 h-8 rounded-lg text-sm font-medium transition-all duration-200"
|
|
519
|
+
:class="page === currentPage
|
|
520
|
+
? 'bg-accent-500 text-surface'
|
|
521
|
+
: 'bg-surface-200 hover:bg-surface-300 text-neutral-300'"
|
|
522
|
+
x-text="page"
|
|
523
|
+
></button>
|
|
524
|
+
</template>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<!-- Next Button -->
|
|
528
|
+
<button
|
|
529
|
+
@click="nextPage()"
|
|
530
|
+
:disabled="currentPage === totalPages"
|
|
531
|
+
class="px-3 py-1.5 bg-surface-200 hover:bg-surface-300 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
532
|
+
>
|
|
533
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
534
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
535
|
+
</svg>
|
|
536
|
+
</button>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
</section>
|
|
541
|
+
|
|
542
|
+
<!-- Footer -->
|
|
543
|
+
<footer class="text-center pt-12 border-t border-surface-200/30 opacity-0 animate-fade-in stagger-4">
|
|
544
|
+
<div class="flex items-center justify-center space-x-2 text-neutral-600">
|
|
545
|
+
<span class="font-mono text-sm">Agent Consultation MCP</span>
|
|
546
|
+
<span class="text-accent-500">•</span>
|
|
547
|
+
<span class="font-mono text-sm">v1.0.0</span>
|
|
548
|
+
</div>
|
|
549
|
+
<p class="text-neutral-600 text-sm mt-2">Configuration changes are saved automatically</p>
|
|
550
|
+
</footer>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<!-- API Key Modal -->
|
|
554
|
+
<div
|
|
555
|
+
x-cloak
|
|
556
|
+
x-show="showModal"
|
|
557
|
+
style="display: none;"
|
|
558
|
+
x-transition:enter="transition ease-out duration-300"
|
|
559
|
+
x-transition:enter-start="opacity-0"
|
|
560
|
+
x-transition:enter-end="opacity-100"
|
|
561
|
+
x-transition:leave="transition ease-in duration-200"
|
|
562
|
+
x-transition:leave-start="opacity-100"
|
|
563
|
+
x-transition:leave-end="opacity-0"
|
|
564
|
+
class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
|
565
|
+
@click.self="closeModal()"
|
|
566
|
+
>
|
|
567
|
+
<div
|
|
568
|
+
x-show="showModal"
|
|
569
|
+
x-transition:enter="transition ease-out duration-300"
|
|
570
|
+
x-transition:enter-start="opacity-0 scale-90 translate-y-4"
|
|
571
|
+
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
|
|
572
|
+
x-transition:leave="transition ease-in duration-200"
|
|
573
|
+
x-transition:leave-start="opacity-100 scale-100"
|
|
574
|
+
x-transition:leave-end="opacity-0 scale-90"
|
|
575
|
+
class="glass-card rounded-2xl max-w-md w-full p-8 border border-surface-300/50"
|
|
576
|
+
>
|
|
577
|
+
<div class="flex items-center mb-6">
|
|
578
|
+
<div class="w-12 h-12 bg-accent-500/10 rounded-xl flex items-center justify-center mr-4">
|
|
579
|
+
<svg class="w-6 h-6 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
580
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
|
581
|
+
</svg>
|
|
582
|
+
</div>
|
|
583
|
+
<div>
|
|
584
|
+
<h3 class="text-xl font-semibold text-neutral-100" x-text="'Configure ' + (modalProvider?.name || '')"></h3>
|
|
585
|
+
<p class="text-sm text-neutral-500">Enter your API key below</p>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
|
|
589
|
+
<div class="space-y-5">
|
|
590
|
+
<div>
|
|
591
|
+
<label class="block text-sm font-medium text-neutral-300 mb-2">API Key</label>
|
|
592
|
+
<input
|
|
593
|
+
type="password"
|
|
594
|
+
x-model="modalApiKey"
|
|
595
|
+
@keyup.enter="saveKey()"
|
|
596
|
+
placeholder="sk-..."
|
|
597
|
+
class="w-full bg-surface-100 border border-surface-300 rounded-xl px-4 py-3 text-neutral-100 placeholder-neutral-600 focus:ring-2 focus:ring-accent-500/50 focus:border-accent-500 transition-all duration-300 font-mono text-sm"
|
|
598
|
+
/>
|
|
599
|
+
<p class="text-xs text-neutral-600 mt-2 flex items-center">
|
|
600
|
+
<svg class="w-3 h-3 mr-1 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
601
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
|
602
|
+
</svg>
|
|
603
|
+
Encrypted with AES-256-GCM before storage
|
|
604
|
+
</p>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<div class="flex gap-3 pt-2">
|
|
608
|
+
<button
|
|
609
|
+
@click="saveKey()"
|
|
610
|
+
:disabled="!modalApiKey || saving"
|
|
611
|
+
class="flex-1 px-5 py-3 bg-accent-600 hover:bg-accent-500 rounded-xl font-semibold transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed btn-accent"
|
|
612
|
+
>
|
|
613
|
+
<span x-show="!saving">Save Key</span>
|
|
614
|
+
<span x-show="saving" class="flex items-center justify-center">
|
|
615
|
+
<svg class="animate-spin h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24">
|
|
616
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
617
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
618
|
+
</svg>
|
|
619
|
+
Saving...
|
|
620
|
+
</span>
|
|
621
|
+
</button>
|
|
622
|
+
<button
|
|
623
|
+
@click="testKeyModal()"
|
|
624
|
+
:disabled="!modalApiKey || testingModal"
|
|
625
|
+
class="px-5 py-3 bg-surface-200 hover:bg-surface-300 rounded-xl font-medium transition-all duration-300 disabled:opacity-50"
|
|
626
|
+
>
|
|
627
|
+
<span x-show="!testingModal">Test</span>
|
|
628
|
+
<span x-show="testingModal">
|
|
629
|
+
<svg class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
|
630
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
631
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
632
|
+
</svg>
|
|
633
|
+
</span>
|
|
634
|
+
</button>
|
|
635
|
+
<button
|
|
636
|
+
@click="closeModal()"
|
|
637
|
+
class="px-5 py-3 bg-surface-200 hover:bg-surface-300 rounded-xl font-medium transition-all duration-300"
|
|
638
|
+
>
|
|
639
|
+
Cancel
|
|
640
|
+
</button>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<!-- Conversation Detail Modal -->
|
|
647
|
+
<div
|
|
648
|
+
x-cloak
|
|
649
|
+
x-show="showConversationModal"
|
|
650
|
+
style="display: none;"
|
|
651
|
+
x-transition:enter="transition ease-out duration-300"
|
|
652
|
+
x-transition:enter-start="opacity-0"
|
|
653
|
+
x-transition:enter-end="opacity-100"
|
|
654
|
+
x-transition:leave="transition ease-in duration-200"
|
|
655
|
+
x-transition:leave-start="opacity-100"
|
|
656
|
+
x-transition:leave-end="opacity-0"
|
|
657
|
+
class="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
|
658
|
+
@click.self="closeConversationModal()"
|
|
659
|
+
@keydown.escape.window="closeConversationModal()"
|
|
660
|
+
>
|
|
661
|
+
<div
|
|
662
|
+
x-show="showConversationModal"
|
|
663
|
+
x-transition:enter="transition ease-out duration-300"
|
|
664
|
+
x-transition:enter-start="opacity-0 scale-90 translate-y-4"
|
|
665
|
+
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
|
|
666
|
+
x-transition:leave="transition ease-in duration-200"
|
|
667
|
+
x-transition:leave-start="opacity-100 scale-100"
|
|
668
|
+
x-transition:leave-end="opacity-0 scale-90"
|
|
669
|
+
class="glass-card rounded-2xl w-full max-w-3xl max-h-[85vh] flex flex-col border border-surface-300/50"
|
|
670
|
+
>
|
|
671
|
+
<!-- Modal Header -->
|
|
672
|
+
<div class="flex items-center justify-between p-5 border-b border-surface-300/30 shrink-0">
|
|
673
|
+
<div class="flex items-center gap-3">
|
|
674
|
+
<div class="w-10 h-10 bg-accent-500/10 rounded-xl flex items-center justify-center">
|
|
675
|
+
<svg class="w-5 h-5 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
676
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
|
677
|
+
</svg>
|
|
678
|
+
</div>
|
|
679
|
+
<div>
|
|
680
|
+
<h3 class="text-lg font-semibold text-neutral-100">Conversation Details</h3>
|
|
681
|
+
<div class="flex items-center gap-2 mt-1">
|
|
682
|
+
<template x-if="selectedConversation">
|
|
683
|
+
<span
|
|
684
|
+
class="px-2 py-0.5 rounded text-[10px] font-semibold uppercase"
|
|
685
|
+
:class="selectedConversation.status === 'active'
|
|
686
|
+
? 'bg-emerald-500/20 text-emerald-400'
|
|
687
|
+
: 'bg-neutral-500/20 text-neutral-400'"
|
|
688
|
+
x-text="selectedConversation.status"
|
|
689
|
+
></span>
|
|
690
|
+
</template>
|
|
691
|
+
<template x-if="selectedConversation">
|
|
692
|
+
<span class="px-2 py-0.5 bg-accent-500/15 text-accent-400 rounded text-xs font-mono" x-text="selectedConversation.model"></span>
|
|
693
|
+
</template>
|
|
694
|
+
<template x-if="selectedConversation">
|
|
695
|
+
<span class="text-xs text-neutral-500" x-text="selectedConversation.messageCount + ' messages'"></span>
|
|
696
|
+
</template>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
<button
|
|
701
|
+
@click="closeConversationModal()"
|
|
702
|
+
class="w-10 h-10 bg-surface-200 hover:bg-surface-300 rounded-xl flex items-center justify-center transition-all duration-200"
|
|
703
|
+
>
|
|
704
|
+
<svg class="w-5 h-5 text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
705
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
706
|
+
</svg>
|
|
707
|
+
</button>
|
|
708
|
+
</div>
|
|
709
|
+
|
|
710
|
+
<!-- Modal Body - Scrollable Messages -->
|
|
711
|
+
<div class="flex-1 overflow-y-auto p-5 space-y-4">
|
|
712
|
+
<template x-if="selectedConversation">
|
|
713
|
+
<template x-for="msg in selectedConversation.messages" :key="msg.id">
|
|
714
|
+
<div class="flex" :class="msg.role === 'user' ? 'justify-end' : 'justify-start'">
|
|
715
|
+
<div
|
|
716
|
+
class="max-w-[85%] rounded-xl px-4 py-3"
|
|
717
|
+
:class="msg.role === 'user'
|
|
718
|
+
? 'bg-accent-600/20 text-neutral-100 border border-accent-500/30'
|
|
719
|
+
: 'bg-surface-200/50 text-neutral-200 border border-surface-300/30'"
|
|
720
|
+
>
|
|
721
|
+
<div class="flex items-center mb-2">
|
|
722
|
+
<div
|
|
723
|
+
class="w-6 h-6 rounded-lg flex items-center justify-center mr-2"
|
|
724
|
+
:class="msg.role === 'user' ? 'bg-accent-500/30' : 'bg-emerald-500/30'"
|
|
725
|
+
>
|
|
726
|
+
<template x-if="msg.role === 'user'">
|
|
727
|
+
<svg class="w-3.5 h-3.5 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
728
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
729
|
+
</svg>
|
|
730
|
+
</template>
|
|
731
|
+
<template x-if="msg.role !== 'user'">
|
|
732
|
+
<svg class="w-3.5 h-3.5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
733
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
734
|
+
</svg>
|
|
735
|
+
</template>
|
|
736
|
+
</div>
|
|
737
|
+
<span class="text-xs font-semibold" :class="msg.role === 'user' ? 'text-accent-400' : 'text-emerald-400'" x-text="msg.role === 'user' ? 'Claude Code' : 'DeepSeek'"></span>
|
|
738
|
+
</div>
|
|
739
|
+
<div class="text-sm whitespace-pre-wrap break-words leading-relaxed" x-text="msg.content"></div>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
</template>
|
|
743
|
+
</template>
|
|
744
|
+
</div>
|
|
745
|
+
|
|
746
|
+
<!-- Modal Footer -->
|
|
747
|
+
<div class="p-4 border-t border-surface-300/30 shrink-0">
|
|
748
|
+
<template x-if="selectedConversation">
|
|
749
|
+
<div class="flex items-center justify-between text-xs text-neutral-500">
|
|
750
|
+
<code class="font-mono bg-surface-200/50 px-2 py-1 rounded" x-text="'ID: ' + selectedConversation.id"></code>
|
|
751
|
+
<div class="flex items-center gap-3">
|
|
752
|
+
<span x-text="'Created: ' + new Date(selectedConversation.createdAt).toLocaleString()"></span>
|
|
753
|
+
<template x-if="selectedConversation.status === 'archived' && selectedConversation.endedAt">
|
|
754
|
+
<span x-text="'Ended: ' + new Date(selectedConversation.endedAt).toLocaleString()"></span>
|
|
755
|
+
</template>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</template>
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
|
|
764
|
+
<script>
|
|
765
|
+
function configApp() {
|
|
766
|
+
return {
|
|
767
|
+
loading: true,
|
|
768
|
+
loadError: null,
|
|
769
|
+
providers: [],
|
|
770
|
+
config: {
|
|
771
|
+
defaultModel: '',
|
|
772
|
+
maxMessages: 5,
|
|
773
|
+
availableModels: [],
|
|
774
|
+
},
|
|
775
|
+
toasts: [],
|
|
776
|
+
showModal: false,
|
|
777
|
+
modalProvider: null,
|
|
778
|
+
modalApiKey: '',
|
|
779
|
+
saving: false,
|
|
780
|
+
testing: null,
|
|
781
|
+
testingModal: false,
|
|
782
|
+
chatHistory: [],
|
|
783
|
+
loadingHistory: false,
|
|
784
|
+
historyStats: { active: 0, archived: 0 },
|
|
785
|
+
// Pagination state
|
|
786
|
+
currentPage: 1,
|
|
787
|
+
itemsPerPage: 10,
|
|
788
|
+
// Conversation detail modal
|
|
789
|
+
selectedConversation: null,
|
|
790
|
+
showConversationModal: false,
|
|
791
|
+
|
|
792
|
+
// Computed: total pages
|
|
793
|
+
get totalPages() {
|
|
794
|
+
return Math.ceil(this.chatHistory.length / this.itemsPerPage) || 1;
|
|
795
|
+
},
|
|
796
|
+
|
|
797
|
+
// Computed: paginated history
|
|
798
|
+
get paginatedHistory() {
|
|
799
|
+
const start = (this.currentPage - 1) * this.itemsPerPage;
|
|
800
|
+
const end = start + this.itemsPerPage;
|
|
801
|
+
return this.chatHistory.slice(start, end);
|
|
802
|
+
},
|
|
803
|
+
|
|
804
|
+
// Pagination methods
|
|
805
|
+
goToPage(page) {
|
|
806
|
+
if (page >= 1 && page <= this.totalPages) {
|
|
807
|
+
this.currentPage = page;
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
nextPage() {
|
|
811
|
+
if (this.currentPage < this.totalPages) {
|
|
812
|
+
this.currentPage++;
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
prevPage() {
|
|
816
|
+
if (this.currentPage > 1) {
|
|
817
|
+
this.currentPage--;
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
|
|
821
|
+
// Conversation modal methods
|
|
822
|
+
openConversationModal(conv) {
|
|
823
|
+
this.selectedConversation = conv;
|
|
824
|
+
this.showConversationModal = true;
|
|
825
|
+
document.body.style.overflow = 'hidden';
|
|
826
|
+
},
|
|
827
|
+
closeConversationModal() {
|
|
828
|
+
this.showConversationModal = false;
|
|
829
|
+
this.selectedConversation = null;
|
|
830
|
+
document.body.style.overflow = '';
|
|
831
|
+
},
|
|
832
|
+
|
|
833
|
+
async init() {
|
|
834
|
+
try {
|
|
835
|
+
await this.loadData();
|
|
836
|
+
// Also load chat history on init
|
|
837
|
+
this.loadChatHistory();
|
|
838
|
+
} catch (error) {
|
|
839
|
+
console.error('Init failed:', error);
|
|
840
|
+
this.loadError = error.message || 'Failed to initialize';
|
|
841
|
+
} finally {
|
|
842
|
+
this.loading = false;
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
|
|
846
|
+
async loadData() {
|
|
847
|
+
const controller = new AbortController();
|
|
848
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
const [providersRes, configRes] = await Promise.all([
|
|
852
|
+
fetch('/api/providers', { signal: controller.signal }),
|
|
853
|
+
fetch('/api/config', { signal: controller.signal }),
|
|
854
|
+
]);
|
|
855
|
+
|
|
856
|
+
if (!providersRes.ok) {
|
|
857
|
+
const errorText = await providersRes.text();
|
|
858
|
+
throw new Error(`Providers API failed (${providersRes.status}): ${errorText}`);
|
|
859
|
+
}
|
|
860
|
+
if (!configRes.ok) {
|
|
861
|
+
const errorText = await configRes.text();
|
|
862
|
+
throw new Error(`Config API failed (${configRes.status}): ${errorText}`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
this.providers = await providersRes.json();
|
|
866
|
+
this.config = await configRes.json();
|
|
867
|
+
this.loadError = null;
|
|
868
|
+
} catch (error) {
|
|
869
|
+
console.error('loadData error:', error);
|
|
870
|
+
if (error.name === 'AbortError') {
|
|
871
|
+
this.showToast('Request timed out. Is the server running?', 'error');
|
|
872
|
+
throw new Error('Request timed out');
|
|
873
|
+
}
|
|
874
|
+
this.showToast('Failed to load: ' + error.message, 'error');
|
|
875
|
+
throw error;
|
|
876
|
+
} finally {
|
|
877
|
+
clearTimeout(timeoutId);
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
|
|
881
|
+
async saveConfig() {
|
|
882
|
+
try {
|
|
883
|
+
const res = await fetch('/api/config', {
|
|
884
|
+
method: 'PATCH',
|
|
885
|
+
headers: { 'Content-Type': 'application/json' },
|
|
886
|
+
body: JSON.stringify({
|
|
887
|
+
defaultModel: this.config.defaultModel,
|
|
888
|
+
maxMessages: parseInt(this.config.maxMessages, 10),
|
|
889
|
+
}),
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
if (!res.ok) {
|
|
893
|
+
const data = await res.json();
|
|
894
|
+
throw new Error(data.message || 'Failed to save');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
this.showToast('Settings saved', 'success');
|
|
898
|
+
} catch (error) {
|
|
899
|
+
this.showToast('Failed to save settings: ' + error.message, 'error');
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
|
|
903
|
+
openKeyModal(provider) {
|
|
904
|
+
this.modalProvider = provider;
|
|
905
|
+
this.modalApiKey = '';
|
|
906
|
+
this.showModal = true;
|
|
907
|
+
},
|
|
908
|
+
|
|
909
|
+
closeModal() {
|
|
910
|
+
this.showModal = false;
|
|
911
|
+
this.modalProvider = null;
|
|
912
|
+
this.modalApiKey = '';
|
|
913
|
+
},
|
|
914
|
+
|
|
915
|
+
async saveKey() {
|
|
916
|
+
if (!this.modalApiKey || !this.modalProvider) return;
|
|
917
|
+
|
|
918
|
+
this.saving = true;
|
|
919
|
+
try {
|
|
920
|
+
const res = await fetch(`/api/providers/${this.modalProvider.id}`, {
|
|
921
|
+
method: 'PUT',
|
|
922
|
+
headers: { 'Content-Type': 'application/json' },
|
|
923
|
+
body: JSON.stringify({ apiKey: this.modalApiKey }),
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
if (!res.ok) {
|
|
927
|
+
const data = await res.json();
|
|
928
|
+
throw new Error(data.message || 'Failed to save');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
this.showToast(`${this.modalProvider.name} API key saved`, 'success');
|
|
932
|
+
this.closeModal();
|
|
933
|
+
await this.loadData();
|
|
934
|
+
} catch (error) {
|
|
935
|
+
this.showToast('Failed to save API key: ' + error.message, 'error');
|
|
936
|
+
} finally {
|
|
937
|
+
this.saving = false;
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
|
|
941
|
+
async testKey(providerId) {
|
|
942
|
+
this.testing = providerId;
|
|
943
|
+
try {
|
|
944
|
+
const res = await fetch(`/api/providers/${providerId}/test`, {
|
|
945
|
+
method: 'POST',
|
|
946
|
+
headers: { 'Content-Type': 'application/json' },
|
|
947
|
+
body: JSON.stringify({}),
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
const data = await res.json();
|
|
951
|
+
if (data.valid) {
|
|
952
|
+
this.showToast(data.message, 'success');
|
|
953
|
+
} else {
|
|
954
|
+
this.showToast(data.message, 'error');
|
|
955
|
+
}
|
|
956
|
+
} catch (error) {
|
|
957
|
+
this.showToast('Test failed: ' + error.message, 'error');
|
|
958
|
+
} finally {
|
|
959
|
+
this.testing = null;
|
|
960
|
+
}
|
|
961
|
+
},
|
|
962
|
+
|
|
963
|
+
async testKeyModal() {
|
|
964
|
+
if (!this.modalApiKey || !this.modalProvider) return;
|
|
965
|
+
|
|
966
|
+
this.testingModal = true;
|
|
967
|
+
try {
|
|
968
|
+
const res = await fetch(`/api/providers/${this.modalProvider.id}/test`, {
|
|
969
|
+
method: 'POST',
|
|
970
|
+
headers: { 'Content-Type': 'application/json' },
|
|
971
|
+
body: JSON.stringify({ apiKey: this.modalApiKey }),
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
const data = await res.json();
|
|
975
|
+
if (data.valid) {
|
|
976
|
+
this.showToast(data.message, 'success');
|
|
977
|
+
} else {
|
|
978
|
+
this.showToast(data.message, 'error');
|
|
979
|
+
}
|
|
980
|
+
} catch (error) {
|
|
981
|
+
this.showToast('Test failed: ' + error.message, 'error');
|
|
982
|
+
} finally {
|
|
983
|
+
this.testingModal = false;
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
|
|
987
|
+
async removeKey(providerId) {
|
|
988
|
+
if (!confirm('Are you sure you want to remove this API key?')) return;
|
|
989
|
+
|
|
990
|
+
try {
|
|
991
|
+
const res = await fetch(`/api/providers/${providerId}`, {
|
|
992
|
+
method: 'DELETE',
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
if (!res.ok) {
|
|
996
|
+
const data = await res.json();
|
|
997
|
+
throw new Error(data.message || 'Failed to remove');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
this.showToast('API key removed', 'success');
|
|
1001
|
+
await this.loadData();
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
this.showToast('Failed to remove API key: ' + error.message, 'error');
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
|
|
1007
|
+
showToast(message, type = 'info') {
|
|
1008
|
+
const id = Date.now();
|
|
1009
|
+
const toast = { id, message, type, show: true };
|
|
1010
|
+
this.toasts.push(toast);
|
|
1011
|
+
|
|
1012
|
+
setTimeout(() => {
|
|
1013
|
+
toast.show = false;
|
|
1014
|
+
setTimeout(() => {
|
|
1015
|
+
this.toasts = this.toasts.filter((t) => t.id !== id);
|
|
1016
|
+
}, 200);
|
|
1017
|
+
}, 3000);
|
|
1018
|
+
},
|
|
1019
|
+
|
|
1020
|
+
async loadChatHistory() {
|
|
1021
|
+
this.loadingHistory = true;
|
|
1022
|
+
try {
|
|
1023
|
+
const res = await fetch('/api/chat/history');
|
|
1024
|
+
if (!res.ok) {
|
|
1025
|
+
throw new Error('Failed to fetch history');
|
|
1026
|
+
}
|
|
1027
|
+
const data = await res.json();
|
|
1028
|
+
this.chatHistory = data.conversations || [];
|
|
1029
|
+
this.historyStats = {
|
|
1030
|
+
active: data.activeCount || 0,
|
|
1031
|
+
archived: data.archivedCount || 0,
|
|
1032
|
+
};
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
console.error('Failed to load chat history:', error);
|
|
1035
|
+
// Silent fail - don't show error toast for history
|
|
1036
|
+
} finally {
|
|
1037
|
+
this.loadingHistory = false;
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
</script>
|
|
1043
|
+
</body>
|
|
1044
|
+
</html>
|