From 0e2cd8c0188ef2d741925a117f8c4120b3622972 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Thu, 4 Dec 2025 13:40:38 +0800 Subject: [PATCH] Feat/kook (#1834) * feat: add adapter file * fix: style for bot log * fix: kook bugs --- src/langbot/pkg/platform/sources/kook.png | Bin 0 -> 14891 bytes src/langbot/pkg/platform/sources/kook.py | 682 ++++++++++++++++++ src/langbot/pkg/platform/sources/kook.yaml | 24 + .../components/bot-log/view/BotLogCard.tsx | 41 +- .../components/bot-log/view/botLog.module.css | 103 ++- 5 files changed, 814 insertions(+), 36 deletions(-) create mode 100644 src/langbot/pkg/platform/sources/kook.png create mode 100644 src/langbot/pkg/platform/sources/kook.py create mode 100644 src/langbot/pkg/platform/sources/kook.yaml diff --git a/src/langbot/pkg/platform/sources/kook.png b/src/langbot/pkg/platform/sources/kook.png new file mode 100644 index 0000000000000000000000000000000000000000..ba6ea15d87bc3de9a88784202814a321141f5b64 GIT binary patch literal 14891 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 tuple[str, int]: + """ + Convert LangBot MessageChain to KOOK message format + + Returns: + tuple: (content, message_type) + - content: message content string + - message_type: 1=text, 2=image, 4=file, 9=KMarkdown + """ + content_parts = [] + message_type = 1 # Default to text + + for component in message_chain: + if isinstance(component, platform_message.Plain): + content_parts.append(component.text) + elif isinstance(component, platform_message.At): + # KOOK mention format: (met)user_id(met) + if component.target: + content_parts.append(f'(met){component.target}(met)') + elif isinstance(component, platform_message.AtAll): + # KOOK @all format: (met)all(met) + content_parts.append('(met)all(met)') + elif isinstance(component, platform_message.Image): + # For images, we need to upload first via KOOK's asset API + # For now, we'll send the image URL if available + if component.url: + content_parts.append(component.url) + message_type = 2 # Image message type + elif isinstance(component, platform_message.Forward): + # Handle forward messages by concatenating content + for node in component.node_list: + forward_content, _ = await KookMessageConverter.yiri2target(node.message_chain) + content_parts.append(forward_content) + # Ignore Source and other components + + content = ''.join(content_parts) + return content, message_type + + @staticmethod + async def target2yiri(kook_message: dict, bot_account_id: str = '') -> platform_message.MessageChain: + """ + Convert KOOK message format to LangBot MessageChain + + Args: + kook_message: KOOK message event data dict + bot_account_id: Bot's account ID for handling role mentions + """ + components = [] + + msg_type = kook_message.get('type', 1) + content = kook_message.get('content', '') + extra = kook_message.get('extra', {}) + + # Handle mentions + mentions = extra.get('mention', []) + mention_all = extra.get('mention_all', False) + mention_roles = extra.get('mention_roles', []) + + if mention_all: + components.append(platform_message.AtAll()) + + for mention_id in mentions: + components.append(platform_message.At(target=str(mention_id))) + + # Handle role mentions (when bot is mentioned via role) + # In KOOK, when a role that the bot has is mentioned, we receive it as a role mention + # We need to convert this to an At with the bot's account ID for the pipeline to recognize it + if mention_roles and bot_account_id: + # Add an At component with the bot's account ID when any role is mentioned + # This is because KOOK bots are often assigned roles and @role mentions should trigger responses + components.append(platform_message.At(target=bot_account_id)) + + # Strip mention patterns from content + # Remove user mention patterns: (met)USER_ID(met) + for mention_id in mentions: + content = content.replace(f'(met){mention_id}(met)', '') + + # Remove @all pattern + if mention_all: + content = content.replace('(met)all(met)', '') + + # Remove role mention patterns: (rol)ROLE_ID(rol) + for role_id in mention_roles: + content = content.replace(f'(rol){role_id}(rol)', '') + + # Clean up extra whitespace + content = content.strip() + + # Handle different message types + if msg_type == 1: # Text message + if content: + components.append(platform_message.Plain(text=content)) + elif msg_type == 2: # Image message + # Image content is typically a URL + if content: + # Download image and convert to base64 + try: + async with aiohttp.ClientSession() as session: + async with session.get(content) as response: + if response.status == 200: + image_bytes = await response.read() + image_base64 = base64.b64encode(image_bytes).decode('utf-8') + # Detect image format + content_type = response.headers.get('Content-Type', 'image/png') + components.append( + platform_message.Image(base64=f'data:{content_type};base64,{image_base64}') + ) + except Exception: + # If download fails, just add as plain text + components.append(platform_message.Plain(text=f'[Image: {content}]')) + elif msg_type == 4: # File message + # For file messages, content is typically the file URL + attachments = extra.get('attachments', {}) + file_name = attachments.get('name', 'file') + components.append(platform_message.Plain(text=f'[File: {file_name}]')) + elif msg_type == 9: # KMarkdown message + # Note: content is already stripped of mention patterns above + if content: + components.append(platform_message.Plain(text=content)) + elif msg_type == 10: # Card message + # Card messages are complex, for now just indicate it's a card + components.append(platform_message.Plain(text='[Card Message]')) + else: + # Other message types, just use content as plain text + if content: + components.append(platform_message.Plain(text=content)) + + return platform_message.MessageChain(components) + + +class KookEventConverter(abstract_platform_adapter.AbstractEventConverter): + """Convert between LangBot events and KOOK events""" + + @staticmethod + async def yiri2target(event: platform_events.MessageEvent): + """Convert LangBot event to KOOK event (not implemented)""" + pass + + @staticmethod + async def target2yiri(kook_event: dict, bot_account_id: str = '') -> platform_events.MessageEvent: + """ + Convert KOOK event to LangBot MessageEvent + + Args: + kook_event: KOOK event data dict containing channel_type, type, etc. + bot_account_id: Bot's account ID for handling role mentions + + Returns: + FriendMessage or GroupMessage depending on channel_type + """ + channel_type = kook_event.get('channel_type') + author_id = kook_event.get('author_id') + target_id = kook_event.get('target_id') + msg_timestamp = kook_event.get('msg_timestamp', int(time.time() * 1000)) + extra = kook_event.get('extra', {}) + + # Convert message to MessageChain + message_chain = await KookMessageConverter.target2yiri(kook_event, bot_account_id) + + # Convert timestamp from milliseconds to seconds + event_time = msg_timestamp / 1000.0 + + if channel_type == 'PERSON': + # Direct/Private message + author = extra.get('author', {}) + author_name = author.get('nickname', author.get('username', str(author_id))) + + return platform_events.FriendMessage( + sender=platform_entities.Friend( + id=str(author_id), + nickname=author_name, + remark=str(author_id), + ), + message_chain=message_chain, + time=event_time, + source_platform_object=kook_event, + ) + elif channel_type == 'GROUP': + # Guild/Server channel message + author = extra.get('author', {}) + author_name = author.get('nickname', author.get('username', str(author_id))) + + # guild_id = extra.get('guild_id', '') + channel_name = extra.get('channel_name', str(target_id)) + + return platform_events.GroupMessage( + sender=platform_entities.GroupMember( + id=str(author_id), + member_name=author_name, + permission=platform_entities.Permission.Member, + group=platform_entities.Group( + id=str(target_id), # Channel ID + name=channel_name, + permission=platform_entities.Permission.Member, + ), + special_title='', + join_timestamp=0, + last_speak_timestamp=0, + mute_time_remaining=0, + ), + message_chain=message_chain, + time=event_time, + source_platform_object=kook_event, + ) + else: + # Fallback to FriendMessage for unknown channel types + return platform_events.FriendMessage( + sender=platform_entities.Friend( + id=str(author_id), + nickname=str(author_id), + remark=str(author_id), + ), + message_chain=message_chain, + time=event_time, + source_platform_object=kook_event, + ) + + +class KookAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + """KOOK platform adapter for LangBot""" + + config: dict + message_converter: KookMessageConverter = KookMessageConverter() + event_converter: KookEventConverter = KookEventConverter() + listeners: typing.Dict[ + typing.Type[platform_events.Event], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], + ] = {} + + # WebSocket connection + ws: typing.Optional[websockets.WebSocketClientProtocol] = pydantic.Field(exclude=True, default=None) + ws_task: typing.Optional[asyncio.Task] = pydantic.Field(exclude=True, default=None) + heartbeat_task: typing.Optional[asyncio.Task] = pydantic.Field(exclude=True, default=None) + running: bool = pydantic.Field(exclude=True, default=False) + + # Connection state + session_id: str = pydantic.Field(exclude=True, default='') + current_sn: int = pydantic.Field(exclude=True, default=0) + gateway_url: str = pydantic.Field(exclude=True, default='') + + # HTTP session + http_session: typing.Optional[aiohttp.ClientSession] = pydantic.Field(exclude=True, default=None) + + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, **kwargs): + # Debug: Track init + with open('/tmp/kook_adapter_init.txt', 'w') as f: + f.write(f'KOOK adapter __init__ called at {time.time()}\n') + + # Validate required config + if 'token' not in config: + raise Exception('KOOK adapter requires "token" in config') + + super().__init__( + config=config, + logger=logger, + bot_account_id='', # Will be set after connection + listeners={}, + **kwargs, + ) + + async def _get_gateway_url(self) -> str: + """Get WebSocket gateway URL from KOOK API""" + base_url = 'https://www.kookapp.cn/api/v3/gateway/index' + + # Always use compression for better performance + params = {'compress': 1} + + headers = { + 'Authorization': f'Bot {self.config["token"]}', + } + + async with aiohttp.ClientSession() as session: + async with session.get(base_url, params=params, headers=headers) as response: + if response.status == 200: + data = await response.json() + if data.get('code') == 0: + gateway_url = data['data']['url'] + return gateway_url + else: + raise Exception(f'Failed to get gateway URL: {data.get("message")}') + else: + raise Exception(f'Failed to get gateway URL: HTTP {response.status}') + + async def _get_bot_user_info(self) -> dict: + """Get bot's own user information from KOOK API""" + base_url = 'https://www.kookapp.cn/api/v3/user/me' + + headers = { + 'Authorization': f'Bot {self.config["token"]}', + } + + async with aiohttp.ClientSession() as session: + async with session.get(base_url, headers=headers) as response: + if response.status == 200: + data = await response.json() + if data.get('code') == 0: + user_info = data['data'] + await self.logger.info( + f'Retrieved bot user info: {user_info.get("username")} (ID: {user_info.get("id")})' + ) + return user_info + else: + raise Exception(f'Failed to get bot user info: {data.get("message")}') + else: + raise Exception(f'Failed to get bot user info: HTTP {response.status}') + + async def _handle_hello(self, data: dict): + """Handle HELLO signal (signal 1)""" + session_id = data.get('session_id', '') + self.session_id = session_id + await self.logger.info(f'KOOK WebSocket HELLO received, session_id: {session_id}') + + async def _handle_event(self, data: dict, sn: int): + """Handle EVENT signal (signal 0)""" + self.current_sn = max(self.current_sn, sn) + + # Check if this is a message event + event_type = data.get('type') + channel_type = data.get('channel_type') + author_id = data.get('author_id') + + # Ignore messages from bot itself to prevent infinite loops + if self.bot_account_id and str(author_id) == self.bot_account_id: + await self.logger.debug(f'Ignoring message from bot itself (author_id: {author_id})') + return + + # Only process text messages (type 1, 2, 4, 9, 10) in GROUP or PERSON channels + if event_type in [1, 2, 4, 9, 10] and channel_type in ['GROUP', 'PERSON']: + try: + # Convert to LangBot event + lb_event = await self.event_converter.target2yiri(data, self.bot_account_id) + + # Call registered listener + event_class = type(lb_event) + if event_class in self.listeners: + await self.listeners[event_class](lb_event, self) + except Exception as e: + await self.logger.error(f'Error handling KOOK event: {e}\n{traceback.format_exc()}') + + async def _handle_pong(self, data: dict): + """Handle PONG signal (signal 3)""" + # PONG received, connection is healthy + pass + + async def _heartbeat_loop(self): + """Send PING every 30 seconds""" + try: + while self.running and self.ws: + await asyncio.sleep(30) + + if self.ws: + try: + ping_msg = { + 's': 2, # PING signal + 'sn': self.current_sn, + } + await self.ws.send(json.dumps(ping_msg)) + await self.logger.debug(f'Sent PING with sn={self.current_sn}') + except Exception: + # Connection closed or send failed, exit loop + break + except asyncio.CancelledError: + pass + except Exception as e: + await self.logger.error(f'Heartbeat error: {e}') + + async def _websocket_loop(self): + """Main WebSocket event loop""" + retry_count = 0 + max_retries = 3 + + while self.running and retry_count < max_retries: + try: + # Get gateway URL if not already retrieved + if not self.gateway_url: + self.gateway_url = await self._get_gateway_url() + + # Connect to WebSocket + await self.logger.info(f'Connecting to KOOK WebSocket: {self.gateway_url}') + async with websockets.connect(self.gateway_url) as ws: + self.ws = ws + await self.logger.info('KOOK WebSocket connected') + + # Start heartbeat + self.heartbeat_task = asyncio.create_task(self._heartbeat_loop()) + + # Wait for HELLO within 6 seconds + try: + hello_msg = await asyncio.wait_for(ws.recv(), timeout=6.0) + + # Handle compressed messages (same as main message loop) + if isinstance(hello_msg, bytes): + # Decompress if compressed + try: + hello_msg = zlib.decompress(hello_msg).decode('utf-8') + except Exception: + # Not compressed or decompression failed + hello_msg = hello_msg.decode('utf-8') + + hello_data = json.loads(hello_msg) + + if hello_data.get('s') == 1: # HELLO signal + await self._handle_hello(hello_data['d']) + else: + raise Exception(f'Expected HELLO signal, got signal {hello_data.get("s")}') + except asyncio.TimeoutError: + raise Exception('Did not receive HELLO within 6 seconds') + + # Reset retry count on successful connection + retry_count = 0 + + # Main message loop + async for message in ws: + if isinstance(message, bytes): + # Decompress if compressed + try: + message = zlib.decompress(message).decode('utf-8') + except Exception: + # Not compressed or decompression failed + message = message.decode('utf-8') + + try: + msg_data = json.loads(message) + signal = msg_data.get('s') + + if signal == 0: # EVENT + data = msg_data.get('d', {}) + sn = msg_data.get('sn', 0) + await self._handle_event(data, sn) + elif signal == 3: # PONG + await self._handle_pong(msg_data.get('d', {})) + elif signal == 5: # RECONNECT + await self.logger.info('Received RECONNECT signal') + break # Break to reconnect + elif signal == 6: # RESUME ACK + await self.logger.info('Resume successful') + except json.JSONDecodeError: + await self.logger.error(f'Failed to parse message: {message}') + except Exception as e: + await self.logger.error(f'Error processing message: {e}\n{traceback.format_exc()}') + + except websockets.exceptions.ConnectionClosed: + await self.logger.warning('KOOK WebSocket connection closed, reconnecting...') + retry_count += 1 + await asyncio.sleep(2**retry_count) # Exponential backoff + except Exception as e: + await self.logger.error(f'KOOK WebSocket error: {e}\n{traceback.format_exc()}') + retry_count += 1 + await asyncio.sleep(2**retry_count) + finally: + # Stop heartbeat + if self.heartbeat_task: + self.heartbeat_task.cancel() + try: + await self.heartbeat_task + except asyncio.CancelledError: + pass + self.ws = None + + if retry_count >= max_retries: + await self.logger.error(f'Failed to connect after {max_retries} retries') + + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): + """Send a message to a channel or user""" + content, msg_type = await self.message_converter.yiri2target(message) + + # Determine endpoint based on target_type + if target_type == 'GROUP': + # Send to channel + url = 'https://www.kookapp.cn/api/v3/message/create' + payload = { + 'target_id': target_id, + 'content': content, + 'type': msg_type, + } + else: # PERSON or default + # Send direct message + url = 'https://www.kookapp.cn/api/v3/direct-message/create' + payload = { + 'target_id': target_id, + 'content': content, + 'type': msg_type, + } + + headers = { + 'Authorization': f'Bot {self.config["token"]}', + 'Content-Type': 'application/json', + } + + try: + if not self.http_session: + self.http_session = aiohttp.ClientSession() + + async with self.http_session.post(url, json=payload, headers=headers) as response: + if response.status == 200: + result = await response.json() + if result.get('code') == 0: + await self.logger.debug(f'Message sent successfully to {target_id}') + else: + await self.logger.error(f'Failed to send message: {result.get("message")}') + else: + await self.logger.error(f'Failed to send message: HTTP {response.status}') + except Exception as e: + await self.logger.error(f'Error sending message: {e}') + + async def reply_message( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, + ): + """Reply to a message""" + content, msg_type = await self.message_converter.yiri2target(message) + + kook_event = message_source.source_platform_object + channel_type = kook_event.get('channel_type') + target_id = kook_event.get('target_id') + msg_id = kook_event.get('msg_id') + + # Determine endpoint based on channel_type + if channel_type == 'GROUP': + url = 'https://www.kookapp.cn/api/v3/message/create' + payload = { + 'target_id': target_id, + 'content': content, + 'type': msg_type, + } + else: # PERSON + url = 'https://www.kookapp.cn/api/v3/direct-message/create' + # For direct messages, we need the chat_code or target_id + author_id = kook_event.get('author_id') + extra = kook_event.get('extra', {}) + chat_code = extra.get('code', '') + + payload = { + 'content': content, + 'type': msg_type, + } + + if chat_code: + payload['chat_code'] = chat_code + else: + payload['target_id'] = str(author_id) + + # Add quote if requested + if quote_origin and msg_id: + payload['quote'] = msg_id + + headers = { + 'Authorization': f'Bot {self.config["token"]}', + 'Content-Type': 'application/json', + } + + try: + if not self.http_session: + self.http_session = aiohttp.ClientSession() + + async with self.http_session.post(url, json=payload, headers=headers) as response: + if response.status == 200: + result = await response.json() + if result.get('code') == 0: + await self.logger.debug('Reply sent successfully') + else: + await self.logger.error(f'Failed to send reply: {result.get("message")}') + else: + await self.logger.error(f'Failed to send reply: HTTP {response.status}') + except Exception as e: + await self.logger.error(f'Error sending reply: {e}') + + async def is_muted(self, group_id: int) -> bool: + """Check if bot is muted in a group (not implemented for KOOK)""" + return False + + def register_listener( + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], + ): + """Register an event listener""" + self.listeners[event_type] = callback + + def unregister_listener( + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], + ): + """Unregister an event listener""" + self.listeners.pop(event_type, None) + + async def run_async(self): + """Start the KOOK adapter""" + # Debug: Track run_async + with open('/tmp/kook_adapter_run.txt', 'w') as f: + f.write(f'KOOK adapter run_async called at {time.time()}\n') + + self.running = True + + try: + # Create HTTP session + self.http_session = aiohttp.ClientSession() + + await self.logger.info('Starting KOOK adapter') + + # Get bot's user information and set bot_account_id + try: + bot_info = await self._get_bot_user_info() + self.bot_account_id = str(bot_info.get('id', '')) + except Exception as e: + await self.logger.error(f'Failed to get bot user info: {e}') + # Continue anyway, but bot will process its own messages + + # Start WebSocket connection + self.ws_task = asyncio.create_task(self._websocket_loop()) + + # Keep running + await self.ws_task + except Exception as e: + await self.logger.error(f'KOOK adapter error: {e}\n{traceback.format_exc()}') + finally: + self.running = False + + async def kill(self) -> bool: + """Stop the KOOK adapter""" + self.running = False + + # Cancel tasks + if self.heartbeat_task: + self.heartbeat_task.cancel() + try: + await self.heartbeat_task + except asyncio.CancelledError: + pass + + if self.ws_task: + self.ws_task.cancel() + try: + await self.ws_task + except asyncio.CancelledError: + pass + + # Close WebSocket + if self.ws: + try: + await self.ws.close() + except Exception: + pass # Already closed or error during close + + # Close HTTP session + if self.http_session: + await self.http_session.close() + + await self.logger.info('KOOK adapter stopped') + return True diff --git a/src/langbot/pkg/platform/sources/kook.yaml b/src/langbot/pkg/platform/sources/kook.yaml new file mode 100644 index 00000000..c0da62c7 --- /dev/null +++ b/src/langbot/pkg/platform/sources/kook.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: kook + label: + en_US: KOOK + zh_Hans: KOOK + description: + en_US: KOOK Adapter (formerly KaiHeiLa) + zh_Hans: KOOK 适配器(原开黑啦),支持频道消息和私聊消息 + icon: kook.png +spec: + config: + - name: token + label: + en_US: Bot Token + zh_Hans: 机器人令牌 + type: string + required: true + default: "" +execution: + python: + path: ./kook.py + attr: KookAdapter diff --git a/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx b/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx index b7fd2e90..25be395a 100644 --- a/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx +++ b/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx @@ -48,7 +48,7 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
{/* 头部标签,时间 */}
-
+
{botLog.level}
{botLog.message_session_id && (
- {/* 会话ID */} {getSubChatId(botLog.message_session_id)} @@ -95,22 +95,25 @@ export function BotLogCard({ botLog }: { botLog: BotLog }) {
)}
-
{formatTime(botLog.timestamp)}
-
-
- {botLog.text} -
- -
- {botLog.images.map((item) => ( - - ))} +
+ {formatTime(botLog.timestamp)}
- +
+
{botLog.text}
+ {botLog.images.length > 0 && ( + +
+ {botLog.images.map((item) => ( + + ))} +
+
+ )}
); } diff --git a/web/src/app/home/bots/components/bot-log/view/botLog.module.css b/web/src/app/home/bots/components/bot-log/view/botLog.module.css index 9cbeffa6..ccaafbb6 100644 --- a/web/src/app/home/bots/components/bot-log/view/botLog.module.css +++ b/web/src/app/home/bots/components/bot-log/view/botLog.module.css @@ -1,21 +1,32 @@ .botLogListContainer { width: 100%; + max-width: 100%; min-height: 10rem; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; - overflow-y: scroll; + overflow-y: auto; + overflow-x: hidden; + box-sizing: border-box; } .botLogCardContainer { width: 100%; + max-width: 100%; background-color: #fff; - border-radius: 10px; - border: 1px solid #cbd5e1; - padding: 1.2rem; - margin-bottom: 1rem; - cursor: pointer; + border-radius: 8px; + border: 1px solid #e2e8f0; + padding: 1rem; + margin-bottom: 0.75rem; + transition: all 0.2s ease; + overflow: hidden; + box-sizing: border-box; +} + +.botLogCardContainer:hover { + border-color: #cbd5e1; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } :global(.dark) .botLogCardContainer { @@ -23,40 +34,74 @@ border: 1px solid #2a2a2e; } +:global(.dark) .botLogCardContainer:hover { + border-color: #3a3a3e; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + .listHeader { width: 100%; height: 2.5rem; display: flex; flex-direction: row; align-items: center; + margin-bottom: 0.5rem; } .tag { - display: flex; + display: inline-flex; flex-direction: row; align-items: center; - justify-content: flex-start; - gap: 0.2rem; - height: 1.5rem; - padding: 0.5rem; - border-radius: 0.4rem; - background-color: #a5d8ff; - color: #ffffff; + justify-content: center; + gap: 0.25rem; + height: auto; + padding: 0.25rem 0.5rem; + border-radius: 4px; + background-color: #dbeafe; + color: #1e40af; + font-size: 0.75rem; + font-weight: 500; max-width: 16rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + text-transform: uppercase; + letter-spacing: 0.025em; +} + +:global(.dark) .tag { + background-color: #1e3a8a; + color: #93c5fd; } .chatTag { - color: #626262; - background-color: #d1d1d1; + color: #4b5563; + background-color: #f3f4f6; + text-transform: none; + cursor: pointer; + transition: all 0.15s ease; +} + +.chatTag:hover { + background-color: #e5e7eb; +} + +:global(.dark) .chatTag { + color: #9ca3af; + background-color: #374151; +} + +:global(.dark) .chatTag:hover { + background-color: #4b5563; } .chatId { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, + 'Courier New', monospace; + font-size: 0.7rem; } .cardTitleContainer { @@ -65,9 +110,33 @@ flex-direction: row; align-items: center; justify-content: space-between; + margin-bottom: 0.5rem; } .cardText { - margin-top: 0.4rem; + color: #1e293b; + font-size: 0.875rem; + line-height: 1.7; + white-space: pre-wrap; + word-wrap: break-word; + word-break: break-all; + overflow-wrap: anywhere; + hyphens: auto; + max-width: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', sans-serif; +} + +:global(.dark) .cardText { + color: #e2e8f0; +} + +.timestamp { + color: #64748b; + font-size: 0.75rem; + white-space: nowrap; +} + +:global(.dark) .timestamp { color: #64748b; }