Просмотр исходного кода

Simplify chat UI: drop send_ready, add refresh button, scope send state

Remove send_ready() from protocol, both clients, API route, and MCP
tool. Send button is always enabled for services that support chat;
errors surface inline on failure with the API error detail message.

Add a reload button (↺) in the Messages subtitle bar. Add 3s post-send
delay before reloading the thread so new messages have time to land.
Scope chatSending to the originating chat_id so navigating away and
back to a reservation doesn't leave a stale spinner on the new view.
ZiB 4 месяцев назад
Родитель
Сommit
cf1e1feef5

+ 0 - 8
room_cal/api/_routes/chat.py

@@ -52,12 +52,6 @@ async def get_chat_templates():
   return {'templates': cfg.templates, 'default_datetime_format': cfg.default_datetime_format}
 
 
-@router.get('/chat/{service_key}/ready')
-async def chat_send_ready(service_key: str):
-  client = _get_chat_client(service_key)
-  return {'ready': client.send_ready()}
-
-
 @router.get('/chat/{service_key}/{chat_id}')
 async def get_thread(service_key: str, chat_id: str):
   client = _get_chat_client(service_key)
@@ -73,8 +67,6 @@ async def get_thread(service_key: str, chat_id: str):
 @router.post('/chat/{service_key}/{chat_id}', status_code=204)
 async def send_message(service_key: str, chat_id: str, payload: _SendBody):
   client = _get_chat_client(service_key)
-  if not client.send_ready():
-    raise HTTPException(503, f'{service_key!r} chat sending is not available')
   try:
     await client.send_message(chat_id, payload.body)
   except ServiceAuthError as e:

+ 5 - 20
room_cal/mcp/__init__.py

@@ -2,7 +2,7 @@ import asyncio
 from datetime import date
 from fastmcp import FastMCP
 from room_cal._utils import services as _services, safe_json_loads, iter_dates
-from room_cal._health import caldav_checks, credentials_checks
+from room_cal._health import store_check, credentials_checks
 from room_cal.pricing import CalPrice
 from room_cal.pricing.calc import (
   net_to_list, list_to_net, list_to_retail,
@@ -24,14 +24,13 @@ _API_SOURCE = {'domain': 'mcp', 'id': ''}
 
 @mcp.tool()
 async def health_check() -> dict:
-  """Check CalDAV connection, room calendar reachability, and service credential validity. Returns {caldav_connection, calendar, credentials}."""
-  (conn, cal_result), creds = await asyncio.gather(
-    caldav_checks(),
+  """Check store connectivity and service credential validity. Returns {store_connected, credentials}."""
+  store_result, creds = await asyncio.gather(
+    store_check(),
     credentials_checks(),
   )
   return {
-    'caldav_connection': conn.to_dict(),
-    'calendar': cal_result.to_dict(),
+    'store_connected': store_result.to_dict(),
     'credentials': {k: v.to_dict() for k, v in creds.items()},
   }
 
@@ -241,20 +240,6 @@ async def get_chat_thread(service_key: str, chat_id: str) -> dict:
   return result
 
 
-@mcp.tool()
-async def chat_send_ready(service_key: str) -> dict:
-  """Check whether sending messages is currently available for a service. Returns {ready: bool}."""
-  from room_cal.service_api import ServiceAPI
-  api = ServiceAPI()
-  try:
-    client = api.chat_client(service_key)
-  except KeyError:
-    raise ValueError(f'unknown service: {service_key!r}')
-  except TypeError:
-    raise ValueError(f'{service_key!r} does not support chat')
-  return {'ready': client.send_ready()}
-
-
 @mcp.tool()
 async def send_chat_message(service_key: str, chat_id: str, body: str) -> None:
   """Send a text message to a guest conversation thread."""

+ 1 - 2
room_cal/service_api/_airbnb.py

@@ -410,8 +410,7 @@ class _AirbnbClient:
   def _global_thread_id(self, thread_id: str) -> str:
     return base64.b64encode(f'MessageThread:{thread_id}'.encode()).decode()
 
-  def send_ready(self) -> bool:
-    return True
+
 
   async def get_thread(self, thread_id: str) -> AirbnbThread:
     """Fetch a conversation thread and its messages by numeric thread ID."""

+ 0 - 3
room_cal/service_api/_booking.py

@@ -591,9 +591,6 @@ class _BookingClient:
       for rid, r_data in reservations.items()
     ]
 
-  def send_ready(self) -> bool:
-    return True
-
   async def get_thread(self, reservation_id: str) -> BookingThread:
     """Fetch a conversation thread and its messages by reservation ID."""
     if is_stale('booking'):

+ 0 - 1
room_cal/service_api/_protocols.py

@@ -19,4 +19,3 @@ class ChatClient(ServiceClient, Protocol):
   # chat_id maps to each platform's native thread/conversation identifier
   async def get_thread(self, chat_id: str) -> Any: ...
   async def send_message(self, chat_id: str, body: str) -> None: ...
-  def send_ready(self) -> bool: ...

+ 26 - 29
room_cal/vue/calendar.html

@@ -619,6 +619,12 @@
     }
     .chat-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
     .chat-send-btn:not(:disabled):hover { opacity: 0.82; }
+    .chat-reload-btn {
+      background: none; border: none; cursor: pointer; font-size: 14px;
+      color: #6b7280; padding: 0 0 0 6px; line-height: 1; vertical-align: middle;
+    }
+    .chat-reload-btn:disabled { opacity: 0.4; cursor: not-allowed; }
+    .chat-reload-btn:not(:disabled):hover { color: #111; }
 
     /* ── Res changes card (no-selection state) ───────────────── */
     .res-changes-card {
@@ -1324,7 +1330,10 @@
         <div v-if="selectionSupportsChat && selection.chat_id" class="panel-card">
           <div class="panel-head">
             <div class="panel-title">Messages</div>
-            <div class="panel-subtitle" v-if="chatThread">{{ visibleChatMessages.length }} message{{ visibleChatMessages.length !== 1 ? 's' : '' }}</div>
+            <div class="panel-subtitle" v-if="chatThread">
+              {{ visibleChatMessages.length }} message{{ visibleChatMessages.length !== 1 ? 's' : '' }}
+              <button class="chat-reload-btn" @click="loadChat" :disabled="chatLoading" title="Refresh messages">↺</button>
+            </div>
           </div>
           <div v-if="chatLoading" class="chat-status">Loading…</div>
           <div v-else-if="chatError" class="chat-status chat-error-txt">{{ chatError }}</div>
@@ -1349,15 +1358,15 @@
               <textarea
                 v-model="chatReply"
                 class="chat-reply-input"
-                :placeholder="chatSendReady ? 'Type a reply…' : 'Sending not available for this service.'"
-                :disabled="!chatSendReady"
+                placeholder="Type a reply…"
+                :disabled="chatSending"
                 rows="6"
               ></textarea>
               <div class="chat-reply-footer">
                 <span v-if="chatSendError" class="chat-send-err">{{ chatSendError }}</span>
                 <button
                   class="chat-send-btn"
-                  :disabled="!chatSendReady || !chatReply.trim() || chatSending"
+                  :disabled="!chatReply.trim() || chatSending"
                   @click="sendChatMessage"
                 >{{ chatSending ? 'Sending…' : 'Send' }}</button>
               </div>
@@ -1478,7 +1487,6 @@ createApp({
     const chatReply         = ref('');
     const chatSending       = ref(false);
     const chatSendError     = ref(null);
-    const chatSendReady     = ref(true);
     const chatTemplates     = ref({});
     const chatTemplatesDtFmt = ref('%a, %b %-d @ %-I %p');
     const syncEnabled   = ref(false);
@@ -1494,10 +1502,8 @@ createApp({
       const c = healthChecks.value;
       if (!c) return [];
       const out = [];
-      if (c.caldav_connection && !c.caldav_connection.ok)
-        out.push(`CalDAV: ${c.caldav_connection.reason}`);
-      if (c.calendar && !c.calendar.ok)
-        out.push(`Calendar: ${c.calendar.reason}`);
+      if (c.store_connected && !c.store_connected.ok)
+        out.push(`Store: ${c.store_connected.reason}`);
       for (const [key, v] of Object.entries(c.credentials || {}))
         if (!v.ok) out.push(`${serviceName(key)}: ${v.reason}`);
       return out;
@@ -1538,23 +1544,11 @@ createApp({
       }
     }
 
-    async function loadChatSendReady(serviceKey) {
-      try {
-        const resp = await apiFetch(`${API_BASE}/chat/${serviceKey}/ready`);
-        if (!resp) return;
-        const data = await resp.json();
-        chatSendReady.value = data.ready ?? true;
-      } catch (e) {
-        chatSendReady.value = false;
-      }
-    }
-
     async function loadChat() {
       const sel = selection.value;
       if (!sel?.chat_id || !sel?.service) return;
       chatLoading.value = true;
       chatError.value = null;
-      loadChatSendReady(sel.service);
       try {
         const resp = await apiFetch(`${API_BASE}/chat/${sel.service}/${sel.chat_id}`);
         if (!resp) { chatLoading.value = false; return; }
@@ -1574,6 +1568,7 @@ createApp({
       if (!body || chatSending.value) return;
       const sel = selection.value;
       if (!sel?.chat_id || !sel?.service) return;
+      const sendChatId = sel.chat_id;
       chatSending.value = true;
       chatSendError.value = null;
       try {
@@ -1582,15 +1577,19 @@ createApp({
           { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body }) }
         );
         if (!resp) { chatSending.value = false; return; }
-        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+        if (!resp.ok) {
+          const detail = await resp.json().catch(() => ({}));
+          throw new Error(detail?.detail || `HTTP ${resp.status}`);
+        }
         chatReply.value = '';
-        await loadChat();
+        await new Promise(r => setTimeout(r, 3000));
+        if (selection.value?.chat_id === sendChatId) await loadChat();
       } catch (e) {
-        chatSendError.value = 'Failed to send.';
-        setTimeout(() => { chatSendError.value = null; }, 4000);
+        chatSendError.value = e.message || 'Failed to send.';
+        setTimeout(() => { chatSendError.value = null; }, 6000);
         console.error('Chat send error:', e);
       } finally {
-        chatSending.value = false;
+        if (selection.value?.chat_id === sendChatId) chatSending.value = false;
       }
     }
 
@@ -1696,7 +1695,6 @@ createApp({
       if (sel?.type === 'res' && sel.chat_id && selectionSupportsChat.value) {
         chatThread.value    = null;
         chatError.value     = null;
-        chatSendReady.value = true;
         loadChat();
       }
     });
@@ -2078,7 +2076,6 @@ createApp({
       chatThread.value    = null;
       chatError.value     = null;
       chatReply.value     = '';
-      chatSendReady.value = true;
       selection.value = {
         type:           'res',
         key:            span.resKey,
@@ -2617,7 +2614,7 @@ createApp({
       bulkEnabled, bulkDays, bulkSortedKeys, bulkPriceSummary, bulkDiscountSummary,
       bulkStatusSummary, bulkActiveDiscountType, bulkToggleClosed,
       CAT, DAYS,
-      chatThread, chatLoading, chatError, chatReply, chatSending, chatSendError, chatSendReady,
+      chatThread, chatLoading, chatError, chatReply, chatSending, chatSendError,
       chatTemplates, selectionSupportsChat, visibleChatMessages, chatSenderLabel, chatMessagesEl,
       loadChat, sendChatMessage, fmtChatTime, applyTemplate,
       runHealthCheck, toggleSync, toggleBulk, fetchDiff, fixDay, fixResChanges, fixBusy, fixDayBusy, fixMonthBusy, FIX,