@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.
Files changed (172) hide show
  1. package/dist/controllers/character.controller.d.ts +10 -0
  2. package/dist/controllers/character.controller.d.ts.map +1 -0
  3. package/dist/controllers/character.controller.js +58 -0
  4. package/dist/controllers/character.controller.js.map +1 -0
  5. package/dist/controllers/chat.controller.d.ts +13 -0
  6. package/dist/controllers/chat.controller.d.ts.map +1 -0
  7. package/dist/controllers/chat.controller.js +113 -0
  8. package/dist/controllers/chat.controller.js.map +1 -0
  9. package/dist/controllers/crisis.controller.d.ts +10 -0
  10. package/dist/controllers/crisis.controller.d.ts.map +1 -0
  11. package/dist/controllers/crisis.controller.js +69 -0
  12. package/dist/controllers/crisis.controller.js.map +1 -0
  13. package/dist/controllers/index.d.ts +10 -0
  14. package/dist/controllers/index.d.ts.map +1 -0
  15. package/dist/controllers/index.js +24 -0
  16. package/dist/controllers/index.js.map +1 -0
  17. package/dist/controllers/leaderboard.controller.d.ts +9 -0
  18. package/dist/controllers/leaderboard.controller.d.ts.map +1 -0
  19. package/dist/controllers/leaderboard.controller.js +51 -0
  20. package/dist/controllers/leaderboard.controller.js.map +1 -0
  21. package/dist/controllers/live-chat.controller.d.ts +14 -0
  22. package/dist/controllers/live-chat.controller.d.ts.map +1 -0
  23. package/dist/controllers/live-chat.controller.js +95 -0
  24. package/dist/controllers/live-chat.controller.js.map +1 -0
  25. package/dist/controllers/matchmaking.controller.d.ts +22 -0
  26. package/dist/controllers/matchmaking.controller.d.ts.map +1 -0
  27. package/dist/controllers/matchmaking.controller.js +157 -0
  28. package/dist/controllers/matchmaking.controller.js.map +1 -0
  29. package/dist/controllers/phrase.controller.d.ts +11 -0
  30. package/dist/controllers/phrase.controller.d.ts.map +1 -0
  31. package/dist/controllers/phrase.controller.js +78 -0
  32. package/dist/controllers/phrase.controller.js.map +1 -0
  33. package/dist/controllers/profile.controller.d.ts +9 -0
  34. package/dist/controllers/profile.controller.d.ts.map +1 -0
  35. package/dist/controllers/profile.controller.js +44 -0
  36. package/dist/controllers/profile.controller.js.map +1 -0
  37. package/dist/controllers/turing.controller.d.ts +18 -0
  38. package/dist/controllers/turing.controller.d.ts.map +1 -0
  39. package/dist/controllers/turing.controller.js +103 -0
  40. package/dist/controllers/turing.controller.js.map +1 -0
  41. package/dist/entities/chat-character.entity.d.ts +34 -0
  42. package/dist/entities/chat-character.entity.d.ts.map +1 -0
  43. package/dist/entities/chat-character.entity.js +101 -0
  44. package/dist/entities/chat-character.entity.js.map +1 -0
  45. package/dist/entities/chat-message.entity.d.ts +26 -0
  46. package/dist/entities/chat-message.entity.d.ts.map +1 -0
  47. package/dist/entities/chat-message.entity.js +84 -0
  48. package/dist/entities/chat-message.entity.js.map +1 -0
  49. package/dist/entities/chat-session.entity.d.ts +31 -0
  50. package/dist/entities/chat-session.entity.d.ts.map +1 -0
  51. package/dist/entities/chat-session.entity.js +95 -0
  52. package/dist/entities/chat-session.entity.js.map +1 -0
  53. package/dist/entities/chat-thread.entity.d.ts +30 -0
  54. package/dist/entities/chat-thread.entity.d.ts.map +1 -0
  55. package/dist/entities/chat-thread.entity.js +105 -0
  56. package/dist/entities/chat-thread.entity.js.map +1 -0
  57. package/dist/entities/crisis-event.entity.d.ts +30 -0
  58. package/dist/entities/crisis-event.entity.d.ts.map +1 -0
  59. package/dist/entities/crisis-event.entity.js +90 -0
  60. package/dist/entities/crisis-event.entity.js.map +1 -0
  61. package/dist/entities/index.d.ts +38 -0
  62. package/dist/entities/index.d.ts.map +1 -0
  63. package/dist/entities/index.js +62 -0
  64. package/dist/entities/index.js.map +1 -0
  65. package/dist/entities/leaderboard-entry.entity.d.ts +22 -0
  66. package/dist/entities/leaderboard-entry.entity.d.ts.map +1 -0
  67. package/dist/entities/leaderboard-entry.entity.js +75 -0
  68. package/dist/entities/leaderboard-entry.entity.js.map +1 -0
  69. package/dist/entities/live-message.entity.d.ts +34 -0
  70. package/dist/entities/live-message.entity.d.ts.map +1 -0
  71. package/dist/entities/live-message.entity.js +99 -0
  72. package/dist/entities/live-message.entity.js.map +1 -0
  73. package/dist/entities/live-thread.entity.d.ts +50 -0
  74. package/dist/entities/live-thread.entity.d.ts.map +1 -0
  75. package/dist/entities/live-thread.entity.js +144 -0
  76. package/dist/entities/live-thread.entity.js.map +1 -0
  77. package/dist/entities/match-room.entity.d.ts +44 -0
  78. package/dist/entities/match-room.entity.d.ts.map +1 -0
  79. package/dist/entities/match-room.entity.js +131 -0
  80. package/dist/entities/match-room.entity.js.map +1 -0
  81. package/dist/entities/phrase-template.entity.d.ts +28 -0
  82. package/dist/entities/phrase-template.entity.d.ts.map +1 -0
  83. package/dist/entities/phrase-template.entity.js +84 -0
  84. package/dist/entities/phrase-template.entity.js.map +1 -0
  85. package/dist/entities/player-profile.entity.d.ts +36 -0
  86. package/dist/entities/player-profile.entity.d.ts.map +1 -0
  87. package/dist/entities/player-profile.entity.js +112 -0
  88. package/dist/entities/player-profile.entity.js.map +1 -0
  89. package/dist/entities/player-queue.entity.d.ts +24 -0
  90. package/dist/entities/player-queue.entity.d.ts.map +1 -0
  91. package/dist/entities/player-queue.entity.js +76 -0
  92. package/dist/entities/player-queue.entity.js.map +1 -0
  93. package/dist/entities/turing-guess.entity.d.ts +29 -0
  94. package/dist/entities/turing-guess.entity.d.ts.map +1 -0
  95. package/dist/entities/turing-guess.entity.js +94 -0
  96. package/dist/entities/turing-guess.entity.js.map +1 -0
  97. package/dist/index.d.ts +14 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +70 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/schemas/sea-king.schema.d.ts +109 -0
  102. package/dist/schemas/sea-king.schema.d.ts.map +1 -0
  103. package/dist/schemas/sea-king.schema.js +103 -0
  104. package/dist/schemas/sea-king.schema.js.map +1 -0
  105. package/dist/sea-king.module.d.ts +19 -0
  106. package/dist/sea-king.module.d.ts.map +1 -0
  107. package/dist/sea-king.module.js +114 -0
  108. package/dist/sea-king.module.js.map +1 -0
  109. package/dist/services/character.service.d.ts +30 -0
  110. package/dist/services/character.service.d.ts.map +1 -0
  111. package/dist/services/character.service.js +75 -0
  112. package/dist/services/character.service.js.map +1 -0
  113. package/dist/services/chat.service.d.ts +49 -0
  114. package/dist/services/chat.service.d.ts.map +1 -0
  115. package/dist/services/chat.service.js +332 -0
  116. package/dist/services/chat.service.js.map +1 -0
  117. package/dist/services/crisis.service.d.ts +17 -0
  118. package/dist/services/crisis.service.d.ts.map +1 -0
  119. package/dist/services/crisis.service.js +177 -0
  120. package/dist/services/crisis.service.js.map +1 -0
  121. package/dist/services/index.d.ts +13 -0
  122. package/dist/services/index.d.ts.map +1 -0
  123. package/dist/services/index.js +28 -0
  124. package/dist/services/index.js.map +1 -0
  125. package/dist/services/leaderboard.service.d.ts +29 -0
  126. package/dist/services/leaderboard.service.d.ts.map +1 -0
  127. package/dist/services/leaderboard.service.js +64 -0
  128. package/dist/services/leaderboard.service.js.map +1 -0
  129. package/dist/services/live-chat.service.d.ts +71 -0
  130. package/dist/services/live-chat.service.d.ts.map +1 -0
  131. package/dist/services/live-chat.service.js +273 -0
  132. package/dist/services/live-chat.service.js.map +1 -0
  133. package/dist/services/matchmaking.service.d.ts +60 -0
  134. package/dist/services/matchmaking.service.d.ts.map +1 -0
  135. package/dist/services/matchmaking.service.js +208 -0
  136. package/dist/services/matchmaking.service.js.map +1 -0
  137. package/dist/services/phrase.service.d.ts +27 -0
  138. package/dist/services/phrase.service.d.ts.map +1 -0
  139. package/dist/services/phrase.service.js +70 -0
  140. package/dist/services/phrase.service.js.map +1 -0
  141. package/dist/services/profile.service.d.ts +41 -0
  142. package/dist/services/profile.service.d.ts.map +1 -0
  143. package/dist/services/profile.service.js +107 -0
  144. package/dist/services/profile.service.js.map +1 -0
  145. package/dist/services/seed.service.d.ts +19 -0
  146. package/dist/services/seed.service.d.ts.map +1 -0
  147. package/dist/services/seed.service.js +550 -0
  148. package/dist/services/seed.service.js.map +1 -0
  149. package/dist/services/turing.service.d.ts +74 -0
  150. package/dist/services/turing.service.d.ts.map +1 -0
  151. package/dist/services/turing.service.js +175 -0
  152. package/dist/services/turing.service.js.map +1 -0
  153. package/dist/services/ws-hub.service.d.ts +51 -0
  154. package/dist/services/ws-hub.service.d.ts.map +1 -0
  155. package/dist/services/ws-hub.service.js +174 -0
  156. package/dist/services/ws-hub.service.js.map +1 -0
  157. package/dist/types.d.ts +12 -0
  158. package/dist/types.d.ts.map +1 -0
  159. package/dist/types.js +8 -0
  160. package/dist/types.js.map +1 -0
  161. package/package.json +69 -0
  162. package/web/index.ts +30 -0
  163. package/web/manifest.ts +40 -0
  164. package/web/messages/en-US.json +113 -0
  165. package/web/messages/zh-CN.json +113 -0
  166. package/web/pages/AnalysisPage.tsx +198 -0
  167. package/web/pages/ChatPage.tsx +482 -0
  168. package/web/pages/DashboardPage.tsx +281 -0
  169. package/web/pages/LeaderboardPage.tsx +208 -0
  170. package/web/pages/PhrasesPage.tsx +221 -0
  171. package/web/services/sea-king-service.ts +151 -0
  172. 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
+ }