@octo-cyber/sea-king 0.5.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/dist/controllers/character.controller.d.ts +10 -0
- package/dist/controllers/character.controller.d.ts.map +1 -0
- package/dist/controllers/character.controller.js +58 -0
- package/dist/controllers/character.controller.js.map +1 -0
- package/dist/controllers/chat.controller.d.ts +13 -0
- package/dist/controllers/chat.controller.d.ts.map +1 -0
- package/dist/controllers/chat.controller.js +113 -0
- package/dist/controllers/chat.controller.js.map +1 -0
- package/dist/controllers/crisis.controller.d.ts +10 -0
- package/dist/controllers/crisis.controller.d.ts.map +1 -0
- package/dist/controllers/crisis.controller.js +69 -0
- package/dist/controllers/crisis.controller.js.map +1 -0
- package/dist/controllers/index.d.ts +10 -0
- package/dist/controllers/index.d.ts.map +1 -0
- package/dist/controllers/index.js +24 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/leaderboard.controller.d.ts +9 -0
- package/dist/controllers/leaderboard.controller.d.ts.map +1 -0
- package/dist/controllers/leaderboard.controller.js +51 -0
- package/dist/controllers/leaderboard.controller.js.map +1 -0
- package/dist/controllers/live-chat.controller.d.ts +14 -0
- package/dist/controllers/live-chat.controller.d.ts.map +1 -0
- package/dist/controllers/live-chat.controller.js +95 -0
- package/dist/controllers/live-chat.controller.js.map +1 -0
- package/dist/controllers/matchmaking.controller.d.ts +22 -0
- package/dist/controllers/matchmaking.controller.d.ts.map +1 -0
- package/dist/controllers/matchmaking.controller.js +157 -0
- package/dist/controllers/matchmaking.controller.js.map +1 -0
- package/dist/controllers/phrase.controller.d.ts +11 -0
- package/dist/controllers/phrase.controller.d.ts.map +1 -0
- package/dist/controllers/phrase.controller.js +78 -0
- package/dist/controllers/phrase.controller.js.map +1 -0
- package/dist/controllers/profile.controller.d.ts +9 -0
- package/dist/controllers/profile.controller.d.ts.map +1 -0
- package/dist/controllers/profile.controller.js +44 -0
- package/dist/controllers/profile.controller.js.map +1 -0
- package/dist/controllers/turing.controller.d.ts +18 -0
- package/dist/controllers/turing.controller.d.ts.map +1 -0
- package/dist/controllers/turing.controller.js +103 -0
- package/dist/controllers/turing.controller.js.map +1 -0
- package/dist/entities/chat-character.entity.d.ts +34 -0
- package/dist/entities/chat-character.entity.d.ts.map +1 -0
- package/dist/entities/chat-character.entity.js +101 -0
- package/dist/entities/chat-character.entity.js.map +1 -0
- package/dist/entities/chat-message.entity.d.ts +26 -0
- package/dist/entities/chat-message.entity.d.ts.map +1 -0
- package/dist/entities/chat-message.entity.js +84 -0
- package/dist/entities/chat-message.entity.js.map +1 -0
- package/dist/entities/chat-session.entity.d.ts +31 -0
- package/dist/entities/chat-session.entity.d.ts.map +1 -0
- package/dist/entities/chat-session.entity.js +95 -0
- package/dist/entities/chat-session.entity.js.map +1 -0
- package/dist/entities/chat-thread.entity.d.ts +30 -0
- package/dist/entities/chat-thread.entity.d.ts.map +1 -0
- package/dist/entities/chat-thread.entity.js +105 -0
- package/dist/entities/chat-thread.entity.js.map +1 -0
- package/dist/entities/crisis-event.entity.d.ts +30 -0
- package/dist/entities/crisis-event.entity.d.ts.map +1 -0
- package/dist/entities/crisis-event.entity.js +90 -0
- package/dist/entities/crisis-event.entity.js.map +1 -0
- package/dist/entities/index.d.ts +38 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +62 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/leaderboard-entry.entity.d.ts +22 -0
- package/dist/entities/leaderboard-entry.entity.d.ts.map +1 -0
- package/dist/entities/leaderboard-entry.entity.js +75 -0
- package/dist/entities/leaderboard-entry.entity.js.map +1 -0
- package/dist/entities/live-message.entity.d.ts +34 -0
- package/dist/entities/live-message.entity.d.ts.map +1 -0
- package/dist/entities/live-message.entity.js +99 -0
- package/dist/entities/live-message.entity.js.map +1 -0
- package/dist/entities/live-thread.entity.d.ts +50 -0
- package/dist/entities/live-thread.entity.d.ts.map +1 -0
- package/dist/entities/live-thread.entity.js +144 -0
- package/dist/entities/live-thread.entity.js.map +1 -0
- package/dist/entities/match-room.entity.d.ts +44 -0
- package/dist/entities/match-room.entity.d.ts.map +1 -0
- package/dist/entities/match-room.entity.js +131 -0
- package/dist/entities/match-room.entity.js.map +1 -0
- package/dist/entities/phrase-template.entity.d.ts +28 -0
- package/dist/entities/phrase-template.entity.d.ts.map +1 -0
- package/dist/entities/phrase-template.entity.js +84 -0
- package/dist/entities/phrase-template.entity.js.map +1 -0
- package/dist/entities/player-profile.entity.d.ts +36 -0
- package/dist/entities/player-profile.entity.d.ts.map +1 -0
- package/dist/entities/player-profile.entity.js +112 -0
- package/dist/entities/player-profile.entity.js.map +1 -0
- package/dist/entities/player-queue.entity.d.ts +24 -0
- package/dist/entities/player-queue.entity.d.ts.map +1 -0
- package/dist/entities/player-queue.entity.js +76 -0
- package/dist/entities/player-queue.entity.js.map +1 -0
- package/dist/entities/turing-guess.entity.d.ts +29 -0
- package/dist/entities/turing-guess.entity.d.ts.map +1 -0
- package/dist/entities/turing-guess.entity.js +94 -0
- package/dist/entities/turing-guess.entity.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/sea-king.schema.d.ts +109 -0
- package/dist/schemas/sea-king.schema.d.ts.map +1 -0
- package/dist/schemas/sea-king.schema.js +103 -0
- package/dist/schemas/sea-king.schema.js.map +1 -0
- package/dist/sea-king.module.d.ts +19 -0
- package/dist/sea-king.module.d.ts.map +1 -0
- package/dist/sea-king.module.js +114 -0
- package/dist/sea-king.module.js.map +1 -0
- package/dist/services/character.service.d.ts +30 -0
- package/dist/services/character.service.d.ts.map +1 -0
- package/dist/services/character.service.js +75 -0
- package/dist/services/character.service.js.map +1 -0
- package/dist/services/chat.service.d.ts +49 -0
- package/dist/services/chat.service.d.ts.map +1 -0
- package/dist/services/chat.service.js +332 -0
- package/dist/services/chat.service.js.map +1 -0
- package/dist/services/crisis.service.d.ts +17 -0
- package/dist/services/crisis.service.d.ts.map +1 -0
- package/dist/services/crisis.service.js +177 -0
- package/dist/services/crisis.service.js.map +1 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +28 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/leaderboard.service.d.ts +29 -0
- package/dist/services/leaderboard.service.d.ts.map +1 -0
- package/dist/services/leaderboard.service.js +64 -0
- package/dist/services/leaderboard.service.js.map +1 -0
- package/dist/services/live-chat.service.d.ts +71 -0
- package/dist/services/live-chat.service.d.ts.map +1 -0
- package/dist/services/live-chat.service.js +273 -0
- package/dist/services/live-chat.service.js.map +1 -0
- package/dist/services/matchmaking.service.d.ts +60 -0
- package/dist/services/matchmaking.service.d.ts.map +1 -0
- package/dist/services/matchmaking.service.js +208 -0
- package/dist/services/matchmaking.service.js.map +1 -0
- package/dist/services/phrase.service.d.ts +27 -0
- package/dist/services/phrase.service.d.ts.map +1 -0
- package/dist/services/phrase.service.js +70 -0
- package/dist/services/phrase.service.js.map +1 -0
- package/dist/services/profile.service.d.ts +41 -0
- package/dist/services/profile.service.d.ts.map +1 -0
- package/dist/services/profile.service.js +107 -0
- package/dist/services/profile.service.js.map +1 -0
- package/dist/services/seed.service.d.ts +19 -0
- package/dist/services/seed.service.d.ts.map +1 -0
- package/dist/services/seed.service.js +550 -0
- package/dist/services/seed.service.js.map +1 -0
- package/dist/services/turing.service.d.ts +74 -0
- package/dist/services/turing.service.d.ts.map +1 -0
- package/dist/services/turing.service.js +175 -0
- package/dist/services/turing.service.js.map +1 -0
- package/dist/services/ws-hub.service.d.ts +51 -0
- package/dist/services/ws-hub.service.d.ts.map +1 -0
- package/dist/services/ws-hub.service.js +174 -0
- package/dist/services/ws-hub.service.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +69 -0
- package/web/index.ts +30 -0
- package/web/manifest.ts +40 -0
- package/web/messages/en-US.json +113 -0
- package/web/messages/zh-CN.json +113 -0
- package/web/pages/AnalysisPage.tsx +198 -0
- package/web/pages/ChatPage.tsx +482 -0
- package/web/pages/DashboardPage.tsx +281 -0
- package/web/pages/LeaderboardPage.tsx +208 -0
- package/web/pages/PhrasesPage.tsx +221 -0
- package/web/services/sea-king-service.ts +151 -0
- package/web/types/sea-king.ts +249 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
5
|
+
import {
|
|
6
|
+
Send,
|
|
7
|
+
Heart,
|
|
8
|
+
HeartCrack,
|
|
9
|
+
AlertTriangle,
|
|
10
|
+
Users,
|
|
11
|
+
Clock,
|
|
12
|
+
Crown,
|
|
13
|
+
X,
|
|
14
|
+
Sparkles,
|
|
15
|
+
MessageCircle,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
import {
|
|
18
|
+
startSession,
|
|
19
|
+
sendMessage,
|
|
20
|
+
triggerCrisis,
|
|
21
|
+
resolveCrisis,
|
|
22
|
+
endSession,
|
|
23
|
+
} from '../services/sea-king-service';
|
|
24
|
+
import type {
|
|
25
|
+
ChatSession,
|
|
26
|
+
ChatThread,
|
|
27
|
+
ChatMessage,
|
|
28
|
+
CrisisEvent,
|
|
29
|
+
SendMessageResult,
|
|
30
|
+
} from '../types/sea-king';
|
|
31
|
+
|
|
32
|
+
const PERSONALITY_COLORS: Record<string, string> = {
|
|
33
|
+
gentle: 'bg-pink-100 text-pink-800 border-pink-300',
|
|
34
|
+
tsundere: 'bg-red-100 text-red-800 border-red-300',
|
|
35
|
+
intellectual: 'bg-indigo-100 text-indigo-800 border-indigo-300',
|
|
36
|
+
rational: 'bg-blue-100 text-blue-800 border-blue-300',
|
|
37
|
+
playful: 'bg-amber-100 text-amber-800 border-amber-300',
|
|
38
|
+
mysterious: 'bg-purple-100 text-purple-800 border-purple-300',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const PERSONALITY_LABELS: Record<string, string> = {
|
|
42
|
+
gentle: 'ๆธฉๆๅ',
|
|
43
|
+
tsundere: 'ๅฒๅจๅ',
|
|
44
|
+
intellectual: 'ๆ่บๅ',
|
|
45
|
+
rational: '็ๆงๅ',
|
|
46
|
+
playful: 'ๆดปๆณผๅ',
|
|
47
|
+
mysterious: '็ฅ็งๅ',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default function ChatPage() {
|
|
51
|
+
const t = useTranslations('seaKing.chat');
|
|
52
|
+
|
|
53
|
+
const [session, setSession] = useState<ChatSession | null>(null);
|
|
54
|
+
const [threads, setThreads] = useState<ChatThread[]>([]);
|
|
55
|
+
const [activeThreadId, setActiveThreadId] = useState<string | null>(null);
|
|
56
|
+
const [messages, setMessages] = useState<Record<string, ChatMessage[]>>({});
|
|
57
|
+
const [inputText, setInputText] = useState('');
|
|
58
|
+
const [sending, setSending] = useState(false);
|
|
59
|
+
const [crisis, setCrisis] = useState<CrisisEvent | null>(null);
|
|
60
|
+
const [crisisInput, setCrisisInput] = useState('');
|
|
61
|
+
const [starting, setStarting] = useState(false);
|
|
62
|
+
const [charCount, setCharCount] = useState(3);
|
|
63
|
+
const [sessionEnded, setSessionEnded] = useState(false);
|
|
64
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
|
|
66
|
+
const scrollToBottom = () => {
|
|
67
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
scrollToBottom();
|
|
72
|
+
}, [messages, activeThreadId]);
|
|
73
|
+
|
|
74
|
+
// ้ๆบ่งฆๅ็ดงๆฅไบไปถ
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!session || session.status !== 'active') return;
|
|
77
|
+
|
|
78
|
+
const timer = setInterval(async () => {
|
|
79
|
+
// 20%ๆฆ็่งฆๅ็ดงๆฅไบไปถ
|
|
80
|
+
if (Math.random() < 0.2 && !crisis) {
|
|
81
|
+
try {
|
|
82
|
+
const newCrisis = await triggerCrisis(session.id);
|
|
83
|
+
setCrisis(newCrisis);
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}, 30000); // ๆฏ30็งๆฃๆฅไธๆฌก
|
|
89
|
+
|
|
90
|
+
return () => clearInterval(timer);
|
|
91
|
+
}, [session, crisis]);
|
|
92
|
+
|
|
93
|
+
const handleStart = async () => {
|
|
94
|
+
setStarting(true);
|
|
95
|
+
try {
|
|
96
|
+
const result = await startSession({ characterCount: charCount, difficulty: 'normal' });
|
|
97
|
+
setSession(result.session);
|
|
98
|
+
setThreads(result.threads);
|
|
99
|
+
setActiveThreadId(result.threads[0]?.id ?? null);
|
|
100
|
+
|
|
101
|
+
// ๅๅงๅๆถๆฏๅ่กจ
|
|
102
|
+
const initialMessages: Record<string, ChatMessage[]> = {};
|
|
103
|
+
for (const thread of result.threads) {
|
|
104
|
+
initialMessages[thread.id] = thread.messages ?? [];
|
|
105
|
+
}
|
|
106
|
+
setMessages(initialMessages);
|
|
107
|
+
} catch (err: any) {
|
|
108
|
+
console.error('Failed to start session:', err);
|
|
109
|
+
} finally {
|
|
110
|
+
setStarting(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleSend = async () => {
|
|
115
|
+
if (!activeThreadId || !inputText.trim() || sending) return;
|
|
116
|
+
|
|
117
|
+
setSending(true);
|
|
118
|
+
const text = inputText;
|
|
119
|
+
setInputText('');
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const result: SendMessageResult = await sendMessage({
|
|
123
|
+
threadId: activeThreadId,
|
|
124
|
+
content: text,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ๆดๆฐๆถๆฏๅ่กจ
|
|
128
|
+
setMessages((prev) => ({
|
|
129
|
+
...prev,
|
|
130
|
+
[activeThreadId]: [
|
|
131
|
+
...(prev[activeThreadId] ?? []),
|
|
132
|
+
result.userMessage,
|
|
133
|
+
result.characterReply,
|
|
134
|
+
],
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// ๆดๆฐ็บฟ็จๅฅฝๆๅบฆ
|
|
138
|
+
setThreads((prev) =>
|
|
139
|
+
prev.map((t) =>
|
|
140
|
+
t.id === activeThreadId
|
|
141
|
+
? {
|
|
142
|
+
...t,
|
|
143
|
+
favorability: result.newFavorability,
|
|
144
|
+
threadStatus: result.threadStatus,
|
|
145
|
+
lastMessage: result.characterReply.content,
|
|
146
|
+
}
|
|
147
|
+
: {
|
|
148
|
+
...t,
|
|
149
|
+
unreadCount: t.unreadCount + (Math.random() > 0.5 ? 1 : 0),
|
|
150
|
+
},
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
} catch (err: any) {
|
|
154
|
+
console.error('Failed to send message:', err);
|
|
155
|
+
} finally {
|
|
156
|
+
setSending(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleResolveCrisis = async () => {
|
|
161
|
+
if (!crisis || !crisisInput.trim()) return;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const resolved = await resolveCrisis({
|
|
165
|
+
crisisId: crisis.id,
|
|
166
|
+
response: crisisInput,
|
|
167
|
+
});
|
|
168
|
+
setCrisis(null);
|
|
169
|
+
setCrisisInput('');
|
|
170
|
+
// ๅทๆฐ็บฟ็จๆฐๆฎไปฅ่ทๅๆดๆฐ็ๅฅฝๆๅบฆ
|
|
171
|
+
} catch (err: any) {
|
|
172
|
+
console.error('Failed to resolve crisis:', err);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleEndSession = async () => {
|
|
177
|
+
if (!session) return;
|
|
178
|
+
try {
|
|
179
|
+
const ended = await endSession(session.id);
|
|
180
|
+
setSession(ended);
|
|
181
|
+
setSessionEnded(true);
|
|
182
|
+
} catch (err: any) {
|
|
183
|
+
console.error('Failed to end session:', err);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const activeThread = threads.find((t) => t.id === activeThreadId);
|
|
188
|
+
const activeMessages = activeThreadId ? messages[activeThreadId] ?? [] : [];
|
|
189
|
+
|
|
190
|
+
// โโโ Start Screen โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
191
|
+
if (!session) {
|
|
192
|
+
return (
|
|
193
|
+
<div className="flex items-center justify-center min-h-[600px] p-6">
|
|
194
|
+
<div className="text-center max-w-lg">
|
|
195
|
+
<div className="text-8xl mb-6 animate-bounce">๐</div>
|
|
196
|
+
<h1 className="text-3xl font-bold bg-gradient-to-r from-cyan-500 to-purple-600 bg-clip-text text-transparent">
|
|
197
|
+
{t('title')}
|
|
198
|
+
</h1>
|
|
199
|
+
<p className="text-gray-500 mt-3 text-lg">{t('startDescription')}</p>
|
|
200
|
+
|
|
201
|
+
<div className="mt-8 bg-gray-50 rounded-xl p-6">
|
|
202
|
+
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
203
|
+
{t('selectCharacterCount')}
|
|
204
|
+
</label>
|
|
205
|
+
<div className="flex items-center justify-center gap-3">
|
|
206
|
+
{[2, 3, 4, 5, 6].map((n) => (
|
|
207
|
+
<button
|
|
208
|
+
key={n}
|
|
209
|
+
onClick={() => setCharCount(n)}
|
|
210
|
+
className={`w-12 h-12 rounded-xl font-bold text-lg transition-all ${
|
|
211
|
+
charCount === n
|
|
212
|
+
? 'bg-gradient-to-br from-cyan-500 to-blue-600 text-white shadow-lg scale-110'
|
|
213
|
+
: 'bg-white border-2 border-gray-200 hover:border-cyan-300 text-gray-600'
|
|
214
|
+
}`}
|
|
215
|
+
>
|
|
216
|
+
{n}
|
|
217
|
+
</button>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
<p className="text-xs text-gray-400 mt-2">
|
|
221
|
+
{t('characterCountHint', { count: charCount })}
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<button
|
|
226
|
+
onClick={handleStart}
|
|
227
|
+
disabled={starting}
|
|
228
|
+
className="mt-6 px-8 py-3 bg-gradient-to-r from-cyan-500 to-purple-600 text-white rounded-xl font-semibold text-lg hover:shadow-xl transition-all hover:-translate-y-1 disabled:opacity-50"
|
|
229
|
+
>
|
|
230
|
+
{starting ? (
|
|
231
|
+
<span className="flex items-center gap-2">
|
|
232
|
+
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white" />
|
|
233
|
+
{t('matching')}
|
|
234
|
+
</span>
|
|
235
|
+
) : (
|
|
236
|
+
<span className="flex items-center gap-2">
|
|
237
|
+
<Sparkles className="h-5 w-5" />
|
|
238
|
+
{t('startTraining')}
|
|
239
|
+
</span>
|
|
240
|
+
)}
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// โโโ Session End Screen โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
248
|
+
if (sessionEnded && session.summary) {
|
|
249
|
+
return (
|
|
250
|
+
<div className="flex items-center justify-center min-h-[600px] p-6">
|
|
251
|
+
<div className="text-center max-w-lg bg-white rounded-2xl shadow-xl p-8 border">
|
|
252
|
+
<div className="text-6xl mb-4">๐</div>
|
|
253
|
+
<h2 className="text-2xl font-bold mb-4">{t('sessionComplete')}</h2>
|
|
254
|
+
<pre className="text-left bg-gray-50 rounded-xl p-4 text-sm whitespace-pre-wrap mb-6">
|
|
255
|
+
{session.summary}
|
|
256
|
+
</pre>
|
|
257
|
+
<button
|
|
258
|
+
onClick={() => {
|
|
259
|
+
setSession(null);
|
|
260
|
+
setThreads([]);
|
|
261
|
+
setMessages({});
|
|
262
|
+
setSessionEnded(false);
|
|
263
|
+
}}
|
|
264
|
+
className="px-6 py-3 bg-gradient-to-r from-cyan-500 to-purple-600 text-white rounded-xl font-semibold hover:shadow-lg transition-all"
|
|
265
|
+
>
|
|
266
|
+
{t('playAgain')} ๐
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// โโโ Main Chat Interface โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
274
|
+
return (
|
|
275
|
+
<div className="flex h-[calc(100vh-120px)] bg-gray-50 rounded-xl overflow-hidden border">
|
|
276
|
+
{/* Thread List (Left Sidebar) */}
|
|
277
|
+
<div className="w-80 bg-white border-r flex flex-col">
|
|
278
|
+
<div className="p-4 border-b bg-gradient-to-r from-cyan-500 to-blue-600">
|
|
279
|
+
<div className="flex items-center justify-between">
|
|
280
|
+
<div className="flex items-center gap-2 text-white">
|
|
281
|
+
<Users className="h-5 w-5" />
|
|
282
|
+
<span className="font-semibold">{t('chatList')}</span>
|
|
283
|
+
</div>
|
|
284
|
+
<button
|
|
285
|
+
onClick={handleEndSession}
|
|
286
|
+
className="text-white/80 hover:text-white text-sm flex items-center gap-1"
|
|
287
|
+
>
|
|
288
|
+
<X className="h-4 w-4" />
|
|
289
|
+
{t('endSession')}
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div className="flex-1 overflow-y-auto">
|
|
295
|
+
{threads.map((thread) => (
|
|
296
|
+
<button
|
|
297
|
+
key={thread.id}
|
|
298
|
+
onClick={() => {
|
|
299
|
+
setActiveThreadId(thread.id);
|
|
300
|
+
setThreads((prev) =>
|
|
301
|
+
prev.map((t) =>
|
|
302
|
+
t.id === thread.id ? { ...t, unreadCount: 0 } : t,
|
|
303
|
+
),
|
|
304
|
+
);
|
|
305
|
+
}}
|
|
306
|
+
className={`w-full p-4 text-left border-b hover:bg-gray-50 transition-colors ${
|
|
307
|
+
activeThreadId === thread.id ? 'bg-cyan-50 border-l-4 border-l-cyan-500' : ''
|
|
308
|
+
} ${thread.threadStatus === 'broken' ? 'opacity-50' : ''}`}
|
|
309
|
+
>
|
|
310
|
+
<div className="flex items-center gap-3">
|
|
311
|
+
<span className="text-2xl">{thread.character.avatar}</span>
|
|
312
|
+
<div className="flex-1 min-w-0">
|
|
313
|
+
<div className="flex items-center justify-between">
|
|
314
|
+
<span className="font-medium truncate">{thread.character.name}</span>
|
|
315
|
+
{thread.unreadCount > 0 && (
|
|
316
|
+
<span className="bg-red-500 text-white text-xs rounded-full px-2 py-0.5">
|
|
317
|
+
{thread.unreadCount}
|
|
318
|
+
</span>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
<div className="text-xs text-gray-400 truncate mt-0.5">
|
|
322
|
+
{thread.lastMessage ?? '...'}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
{/* Favorability Bar */}
|
|
327
|
+
<div className="mt-2 flex items-center gap-2">
|
|
328
|
+
<span className="text-xs">
|
|
329
|
+
{thread.favorability > 70 ? '๐' : thread.favorability > 30 ? '๐' : '๐'}
|
|
330
|
+
</span>
|
|
331
|
+
<div className="flex-1 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
|
332
|
+
<div
|
|
333
|
+
className={`h-full rounded-full transition-all duration-500 ${
|
|
334
|
+
thread.favorability > 70
|
|
335
|
+
? 'bg-pink-500'
|
|
336
|
+
: thread.favorability > 30
|
|
337
|
+
? 'bg-amber-400'
|
|
338
|
+
: 'bg-red-500'
|
|
339
|
+
}`}
|
|
340
|
+
style={{ width: `${thread.favorability}%` }}
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
<span className="text-xs font-mono text-gray-500">{thread.favorability}</span>
|
|
344
|
+
</div>
|
|
345
|
+
{/* Personality Tag */}
|
|
346
|
+
<div className="mt-1">
|
|
347
|
+
<span
|
|
348
|
+
className={`inline-block text-xs px-2 py-0.5 rounded-full border ${
|
|
349
|
+
PERSONALITY_COLORS[thread.character.personality] ?? 'bg-gray-100 text-gray-600'
|
|
350
|
+
}`}
|
|
351
|
+
>
|
|
352
|
+
{PERSONALITY_LABELS[thread.character.personality] ?? thread.character.personality}
|
|
353
|
+
</span>
|
|
354
|
+
{thread.threadStatus === 'broken' && (
|
|
355
|
+
<span className="inline-block text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-600 ml-1">
|
|
356
|
+
ๅทฒๆ้ป
|
|
357
|
+
</span>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
</button>
|
|
361
|
+
))}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
{/* Chat Area (Right) */}
|
|
366
|
+
<div className="flex-1 flex flex-col">
|
|
367
|
+
{/* Chat Header */}
|
|
368
|
+
{activeThread && (
|
|
369
|
+
<div className="p-4 border-b bg-white flex items-center justify-between">
|
|
370
|
+
<div className="flex items-center gap-3">
|
|
371
|
+
<span className="text-3xl">{activeThread.character.avatar}</span>
|
|
372
|
+
<div>
|
|
373
|
+
<div className="font-semibold">{activeThread.character.name}</div>
|
|
374
|
+
<div className="text-xs text-gray-400">
|
|
375
|
+
{activeThread.character.backstory.slice(0, 50)}...
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
<div className="flex items-center gap-2">
|
|
380
|
+
{activeThread.favorability > 70 ? (
|
|
381
|
+
<Heart className="h-5 w-5 text-pink-500 fill-pink-500" />
|
|
382
|
+
) : activeThread.favorability < 30 ? (
|
|
383
|
+
<HeartCrack className="h-5 w-5 text-red-500" />
|
|
384
|
+
) : (
|
|
385
|
+
<Heart className="h-5 w-5 text-gray-300" />
|
|
386
|
+
)}
|
|
387
|
+
<span className="font-mono font-bold text-lg">{activeThread.favorability}</span>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
|
|
392
|
+
{/* Crisis Alert */}
|
|
393
|
+
{crisis && (
|
|
394
|
+
<div className="mx-4 mt-4 bg-red-50 border-2 border-red-300 rounded-xl p-4 animate-pulse">
|
|
395
|
+
<div className="flex items-center gap-2 mb-2">
|
|
396
|
+
<AlertTriangle className="h-5 w-5 text-red-500" />
|
|
397
|
+
<span className="font-bold text-red-700">{t('crisisAlert')}</span>
|
|
398
|
+
<Clock className="h-4 w-4 text-red-400 ml-auto" />
|
|
399
|
+
<span className="text-sm text-red-400">{crisis.timeLimit}s</span>
|
|
400
|
+
</div>
|
|
401
|
+
<p className="text-red-800 mb-3">{crisis.description}</p>
|
|
402
|
+
<div className="flex gap-2">
|
|
403
|
+
<input
|
|
404
|
+
value={crisisInput}
|
|
405
|
+
onChange={(e) => setCrisisInput(e.target.value)}
|
|
406
|
+
placeholder={t('crisisPlaceholder')}
|
|
407
|
+
className="flex-1 px-3 py-2 border border-red-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-400"
|
|
408
|
+
onKeyDown={(e) => e.key === 'Enter' && handleResolveCrisis()}
|
|
409
|
+
/>
|
|
410
|
+
<button
|
|
411
|
+
onClick={handleResolveCrisis}
|
|
412
|
+
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors font-medium"
|
|
413
|
+
>
|
|
414
|
+
{t('respond')}
|
|
415
|
+
</button>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
{/* Messages */}
|
|
421
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
422
|
+
{activeMessages.map((msg) => (
|
|
423
|
+
<div
|
|
424
|
+
key={msg.id}
|
|
425
|
+
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
426
|
+
>
|
|
427
|
+
<div
|
|
428
|
+
className={`max-w-[70%] rounded-2xl px-4 py-3 ${
|
|
429
|
+
msg.role === 'user'
|
|
430
|
+
? 'bg-gradient-to-r from-cyan-500 to-blue-500 text-white'
|
|
431
|
+
: 'bg-white border shadow-sm'
|
|
432
|
+
}`}
|
|
433
|
+
>
|
|
434
|
+
<p className="text-sm">{msg.content}</p>
|
|
435
|
+
{msg.favorDelta !== 0 && msg.role === 'character' && (
|
|
436
|
+
<div
|
|
437
|
+
className={`text-xs mt-2 pt-2 border-t ${
|
|
438
|
+
msg.favorDelta > 0
|
|
439
|
+
? 'text-green-600 border-green-200'
|
|
440
|
+
: 'text-red-500 border-red-200'
|
|
441
|
+
}`}
|
|
442
|
+
>
|
|
443
|
+
{msg.favorDelta > 0 ? '๐' : '๐'} {msg.favorDelta > 0 ? '+' : ''}
|
|
444
|
+
{msg.favorDelta} ยท {msg.favorReason}
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
))}
|
|
450
|
+
<div ref={messagesEndRef} />
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{/* Input Area */}
|
|
454
|
+
<div className="p-4 border-t bg-white">
|
|
455
|
+
{activeThread?.threadStatus === 'broken' ? (
|
|
456
|
+
<div className="text-center text-red-500 py-2">
|
|
457
|
+
๐ {t('blocked')}
|
|
458
|
+
</div>
|
|
459
|
+
) : (
|
|
460
|
+
<div className="flex gap-3">
|
|
461
|
+
<input
|
|
462
|
+
value={inputText}
|
|
463
|
+
onChange={(e) => setInputText(e.target.value)}
|
|
464
|
+
placeholder={t('inputPlaceholder')}
|
|
465
|
+
className="flex-1 px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-cyan-400 focus:border-transparent"
|
|
466
|
+
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()}
|
|
467
|
+
disabled={sending}
|
|
468
|
+
/>
|
|
469
|
+
<button
|
|
470
|
+
onClick={handleSend}
|
|
471
|
+
disabled={sending || !inputText.trim()}
|
|
472
|
+
className="px-5 py-3 bg-gradient-to-r from-cyan-500 to-blue-500 text-white rounded-xl hover:shadow-lg transition-all disabled:opacity-50"
|
|
473
|
+
>
|
|
474
|
+
<Send className="h-5 w-5" />
|
|
475
|
+
</button>
|
|
476
|
+
</div>
|
|
477
|
+
)}
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
}
|