// assistant.jsx — global AI helper, first pass

function AssistantFloatingChat({ go, currentTripId }) {
  const isTripDetail = !!currentTripId;
  const [open, setOpen] = React.useState(false);
  const [pos, setPos] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem('tabi_assistant_pos') || 'null') || { right: 18, bottom: 112 }; }
    catch { return { right: 18, bottom: 112 }; }
  });
  const [dragging, setDragging] = React.useState(false);
  const [context, setContext] = React.useState({ trips: [], default_trip_id: null });
  const [tripId, setTripId] = React.useState(() => localStorage.getItem('tabi_assistant_trip_id') || '');
  const [conversationId, setConversationId] = React.useState(() => localStorage.getItem('tabi_assistant_conversation_id') || '');
  const [conversations, setConversations] = React.useState([]);
  const [showHistory, setShowHistory] = React.useState(false);
  const [messages, setMessages] = React.useState([]);
  const [input, setInput] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [toolCalls, setToolCalls] = React.useState([]);
  const toolCallsRef = React.useRef([]);
  const [streamingText, setStreamingText] = React.useState('');
  const streamingTextRef = React.useRef('');
  const [expandedToolCalls, setExpandedToolCalls] = React.useState(() => new Set());
  const [draft, setDraft] = React.useState(null);
  const [draftPayload, setDraftPayload] = React.useState(null);
  const [draftExpanded, setDraftExpanded] = React.useState(false);
  const [savedExpenseId, setSavedExpenseId] = React.useState(null);
  const [listening, setListening] = React.useState(false);
  const boxRef = React.useRef(null);
  const draftRef = React.useRef(null);
  const dragRef = React.useRef(null);
  const posRef = React.useRef(pos);
  const fileRef = React.useRef(null);
  const recognitionRef = React.useRef(null);

  const loadContext = React.useCallback(() => {
    const query = currentTripId || tripId;
    fetch(`/api/assistant/context${query ? `?trip_id=${encodeURIComponent(query)}` : ''}`, {
      headers: { Authorization: `Bearer ${window.tabiToken()}` },
    })
      .then(r => r.ok ? r.json() : null)
      .then(d => {
        if (!d) return;
        setContext(d);
        // tripId may be stale (e.g. trip was deleted) — fall back if it's no longer valid
        const knownIds = new Set((d.trips || []).map(t => t.id));
        const currentTrip = tripId && knownIds.has(tripId) ? tripId : '';
        const nextTrip = currentTrip || currentTripId || d.default_trip_id || '';
        if (nextTrip !== tripId) {
          setTripId(nextTrip);
          if (nextTrip) localStorage.setItem('tabi_assistant_trip_id', nextTrip);
          else localStorage.removeItem('tabi_assistant_trip_id');
        }
      })
      .catch(() => {});
  }, [currentTripId, tripId]);

  const normalizeMessage = React.useCallback((m) => {
    const meta = m.metadata || m.meta || {};
    return {
      id: m.id,
      role: m.role,
      text: m.text || '',
      metadata: meta,
      action: m.action || meta.action,
      draft: m.draft || meta.draft,
      stats: m.stats || meta.stats,
      invite: m.invite || meta.invite,
      artifact: m.artifact || meta.artifact,
      result: m.result || meta.result,
      imageUrl: m.imageUrl,
      created_at: m.created_at,
    };
  }, []);

  const loadConversation = React.useCallback((id) => {
    if (!id) return Promise.resolve(null);
    return fetch(`/api/assistant/conversations/${id}`, {
      headers: { Authorization: `Bearer ${window.tabiToken()}` },
    })
      .then(r => r.ok ? r.json() : null)
      .then(data => {
        if (!data) return null;
        setConversationId(data.id);
        localStorage.setItem('tabi_assistant_conversation_id', data.id);
        setMessages((data.messages || []).map(normalizeMessage));
        if (data.default_trip_id) {
          setTripId(data.default_trip_id);
          localStorage.setItem('tabi_assistant_trip_id', data.default_trip_id);
        }
        setDraft(null);
        setDraftPayload(null);
        setDraftExpanded(false);
        setSavedExpenseId(null);
        return data;
      });
  }, [normalizeMessage]);

  const createConversation = React.useCallback((defaultTripId = tripId || currentTripId || null) => {
    return fetch('/api/assistant/conversations', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
      body: JSON.stringify({ default_trip_id: defaultTripId || null }),
    })
      .then(r => r.ok ? r.json() : Promise.reject(new Error('建立對話失敗')))
      .then(data => {
        setConversations(prev => [data, ...prev.filter(c => c.id !== data.id)]);
        setShowHistory(false);
        return loadConversation(data.id).then(() => data.id);
      });
  }, [currentTripId, loadConversation, tripId]);

  const deleteConversation = React.useCallback((id) => {
    return fetch(`/api/assistant/conversations/${id}`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${window.tabiToken()}` },
    })
      .then(r => r.ok ? r.json() : Promise.reject(new Error('刪除失敗')))
      .then(async () => {
        const remaining = conversations.filter(c => c.id !== id);
        setConversations(remaining);
        if (id === conversationId) {
          if (remaining.length > 0) {
            await loadConversation(remaining[0].id);
          } else {
            await createConversation(tripId || currentTripId || null);
          }
        }
      });
  }, [conversations, conversationId, loadConversation, createConversation, tripId, currentTripId]);

  const loadConversations = React.useCallback(() => {
    return fetch('/api/assistant/conversations', {
      headers: { Authorization: `Bearer ${window.tabiToken()}` },
    })
      .then(r => r.ok ? r.json() : { conversations: [] })
      .then(async data => {
        const list = data.conversations || [];
        setConversations(list);
        const stored = localStorage.getItem('tabi_assistant_conversation_id') || conversationId;
        const selected = list.find(c => c.id === stored) || list[0];
        if (selected) {
          await loadConversation(selected.id);
          return selected.id;
        }
        return createConversation(currentTripId || tripId || null);
      })
      .catch(() => null);
  }, [conversationId, createConversation, currentTripId, loadConversation, tripId]);

  React.useEffect(() => {
    if (!open) return;
    loadContext();
    loadConversations();
  }, [open]);
  React.useEffect(() => { posRef.current = pos; }, [pos]);
  React.useEffect(() => {
    if (!boxRef.current) return;
    requestAnimationFrame(() => {
      if (draftRef.current) {
        draftRef.current.scrollIntoView({ block: 'start' });
      } else if (boxRef.current) {
        boxRef.current.scrollTop = boxRef.current.scrollHeight;
      }
    });
  }, [messages.length, !!draft, open]);

  const clampAssistantPos = (next) => {
    const buttonSize = 72;
    const minBottom = isTripDetail ? 172 : 112;
    const maxRight = Math.max(10, window.innerWidth - buttonSize - 10);
    const maxBottom = Math.max(minBottom, window.innerHeight - buttonSize - 10);
    const clamped = {
      right: Math.max(10, Math.min(maxRight, next.right)),
      bottom: Math.max(minBottom, Math.min(maxBottom, next.bottom)),
    };
    const nearExpenseFab = isTripDetail && clamped.right < 96 && clamped.bottom < 172;
    return nearExpenseFab ? { ...clamped, bottom: 172 } : clamped;
  };

  React.useEffect(() => {
    const sync = () => {
      const next = clampAssistantPos(posRef.current);
      posRef.current = next;
      setPos(next);
      localStorage.setItem('tabi_assistant_pos', JSON.stringify(next));
    };
    sync();
    window.addEventListener('resize', sync);
    return () => window.removeEventListener('resize', sync);
  }, [isTripDetail]);

  const onPointerDown = (e) => {
    if (open) return;
    e.preventDefault();
    setDragging(true);
    dragRef.current = {
      pointerId: e.pointerId,
      startX: e.clientX,
      startY: e.clientY,
      startRight: posRef.current.right,
      startBottom: posRef.current.bottom,
      moved: false,
    };
    e.currentTarget.setPointerCapture?.(e.pointerId);
  };
  const onPointerMove = (e) => {
    if (!dragging || !dragRef.current || dragRef.current.pointerId !== e.pointerId) return;
    e.preventDefault();
    const dx = e.clientX - dragRef.current.startX;
    const dy = e.clientY - dragRef.current.startY;
    if (Math.hypot(dx, dy) > 7) dragRef.current.moved = true;
    const next = clampAssistantPos({
      right: dragRef.current.startRight - dx,
      bottom: dragRef.current.startBottom - dy,
    });
    posRef.current = next;
    setPos(next);
  };
  const finishDrag = (e) => {
    if (!dragRef.current || (e?.pointerId != null && dragRef.current.pointerId !== e.pointerId)) return;
    const moved = dragRef.current?.moved;
    const finalPos = posRef.current;
    setDragging(false);
    if (e?.currentTarget?.releasePointerCapture && e.pointerId != null) {
      try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
    }
    localStorage.setItem('tabi_assistant_pos', JSON.stringify(finalPos));
    if (!moved) setOpen(true);
    dragRef.current = null;
  };
  const onPointerUp = (e) => finishDrag(e);
  const onPointerCancel = (e) => {
    if (!dragRef.current || dragRef.current.pointerId !== e.pointerId) return;
    setDragging(false);
    dragRef.current = null;
  };

  function selectedTrip() {
    return context.trips.find(t => t.id === tripId) || context.trips.find(t => t.id === context.default_trip_id) || context.trips[0] || null;
  }

  const quickActions = [
    { icon: '探', color: TABI_COLORS.red, label: '查附近', text: '用我目前位置查附近推薦餐廳' },
    { icon: '計', color: TABI_COLORS.blue, label: '今日支出', text: '幫我看這趟旅程今日支出統計' },
    { icon: '購', color: TABI_COLORS.green, label: '購物清單', text: '幫我看購物清單' },
    { icon: '備', color: TABI_COLORS.ink2, label: '行前準備', text: '幫我看行前準備清單' },
  ];

  const assistantExamples = [
    '今天晚餐 1200 日圓，個人支出',
    '新增眼藥水到購物清單',
    '把護照影本勾完成',
  ];

  const greetingWord = (() => {
    const h = new Date().getHours();
    if (h < 5) return '夜深了';
    if (h < 11) return '早安';
    if (h < 14) return '午安';
    if (h < 18) return '下午好';
    return '晚安';
  })();

  function useQuickAction(text) {
    if (busy) return;
    sendMessage(text);
  }

  function runResultRowAction(action) {
    if (!action || !go) return;
    if (action.type === 'shopping_detail' && action.id) {
      setOpen(false);
      go('shopping_detail', action.id);
    }
    if (action.type === 'shopping_trip' && action.id) {
      setOpen(false);
      go('shopping_trip', action.id);
    }
    if (action.type === 'shopping' || action.type === 'shopping_list') {
      setOpen(false);
      go('shopping');
    }
    if (action.type === 'checklist') {
      setOpen(false);
      go('checklist');
    }
  }

  function normalizeAssistantText(text) {
    return String(text || '')
      .replace(/共通支出|公共支出|共同之出|共用支出/g, '共同支出')
      .replace(/個人之出|個人之處|私人支出/g, '個人支出')
      .replace(/ㄧ蘭|衣蘭|一欄/g, '一蘭')
      .replace(/日園|日元/g, '日圓')
      .replace(/台弊|台幤/g, '台幣')
      .replace(/均份/g, '均分')
      .replace(/自定金額|自訂金鵝/g, '自訂金額');
  }

  function shouldUseLocation(text) {
    // Only request the user's GPS for "near me" style queries. Generic
    // category words (餐廳/景點/咖啡...) alone must NOT trigger this —
    // a query like "大阪有什麼餐廳" names its own location and should go
    // through web search, not Google Maps "nearby" using the user's GPS.
    return /附近|周邊|周遭|身邊|這邊|這裡/.test(String(text || ''));
  }

  function requestCurrentLocation() {
    if (!navigator.geolocation) return Promise.resolve({ location: null, reason: 'unsupported' });
    return new Promise(resolve => {
      navigator.geolocation.getCurrentPosition(
        pos => resolve({
          location: {
            lat: pos.coords.latitude,
            lng: pos.coords.longitude,
            accuracy: pos.coords.accuracy,
          },
          reason: null,
        }),
        err => resolve({ location: null, reason: err?.code === 1 ? 'denied' : err?.code === 3 ? 'timeout' : 'unavailable' }),
        { enableHighAccuracy: false, timeout: 6500, maximumAge: 10 * 60 * 1000 },
      );
    });
  }

  function locationHelpMessage(reason) {
    if (reason === 'unsupported') return '這個瀏覽器不支援定位。你可以改說「熱海車站附近晚餐」或「大阪難波附近餐廳」。';
    if (reason === 'denied') return '我沒有取得定位權限，所以不能用 Google Maps 查附近。請到瀏覽器網站設定把「位置」改成允許，重新整理後再試。也可以直接輸入地點，例如「熱海車站附近晚餐」。';
    if (reason === 'timeout') return '定位等太久沒有回來。請確認手機定位已開啟，或改輸入地點，例如「熱海車站附近晚餐」。';
    return '我沒有取得目前位置，所以不能查真正的附近。請允許定位，或改輸入明確地點。';
  }

  function pushToolCallStart(payload) {
    setToolCalls(prev => {
      const next = [
        ...prev,
        {
          id: `${payload.tool || 'tool'}-${Date.now()}-${prev.length}`,
          tool: payload.tool,
          label: payload.tool_label || payload.tool || '工具',
          status: 'running',
          input: payload.input || null,
          output: null,
        },
      ];
      toolCallsRef.current = next;
      return next;
    });
  }

  function applyToolCallDone(payload) {
    setToolCalls(prev => {
      const idx = [...prev].reverse().findIndex(t => t.tool === payload.tool && t.status === 'running');
      if (idx === -1) return prev;
      const realIdx = prev.length - 1 - idx;
      const next = [...prev];
      next[realIdx] = { ...next[realIdx], status: 'done', output: payload.output || null };
      toolCallsRef.current = next;
      return next;
    });
  }

  function appendStreamDelta(delta) {
    if (!delta) return;
    streamingTextRef.current += delta;
    setStreamingText(streamingTextRef.current);
  }

  function applyAssistantResponse(data) {
    if (data.conversation) {
      setConversationId(data.conversation.id);
      localStorage.setItem('tabi_assistant_conversation_id', data.conversation.id);
      setConversations(prev => [data.conversation, ...prev.filter(c => c.id !== data.conversation.id)]);
    }
    const finishedToolCalls = toolCallsRef.current;
    toolCallsRef.current = [];
    setToolCalls([]);
    streamingTextRef.current = '';
    setStreamingText('');
    const assistantMsgBase = data.assistant_message ? normalizeMessage(data.assistant_message) : null;
    const assistantMsg = assistantMsgBase && finishedToolCalls.length > 0
      ? { ...assistantMsgBase, toolCalls: finishedToolCalls }
      : assistantMsgBase;
    const withToolCalls = (msg) => finishedToolCalls.length > 0 ? { ...msg, toolCalls: finishedToolCalls } : msg;
    if (data.action === 'expense_draft' && data.draft) {
      setDraft(data.draft);
      setDraftPayload(data.draft.payload || null);
      if (data.draft.trip?.id) {
        setTripId(data.draft.trip.id);
        localStorage.setItem('tabi_assistant_trip_id', data.draft.trip.id);
      }
      setSavedExpenseId(null);
      setDraftExpanded(true);
      setMessages(prev => [...prev, assistantMsg || withToolCalls({ role: 'assistant', text: `我整理成一筆支出草稿：${data.draft.summary}`, draft: data.draft })]);
    } else if (data.action === 'trip_stats') {
      setMessages(prev => [...prev, assistantMsg || withToolCalls({ role: 'assistant', text: data.message || '已整理旅程統計。', stats: data.stats })]);
    } else if (data.action === 'trip_invite') {
      setMessages(prev => [...prev, assistantMsg || withToolCalls({ role: 'assistant', text: data.message || '已準備邀請連結。', invite: data.invite })]);
    } else if (data.action === 'artifact') {
      setMessages(prev => [...prev, assistantMsg || withToolCalls({ role: 'assistant', text: data.message || '已產生報告。', artifact: data.artifact })]);
    } else if (data.result) {
      setMessages(prev => [...prev, assistantMsg || withToolCalls({ role: 'assistant', text: data.message || '已完成。', result: data.result })]);
    } else {
      setMessages(prev => [...prev, assistantMsg || withToolCalls({ role: 'assistant', text: data.message || '我收到你的需求了。' })]);
    }
  }

  async function readAssistantStream(response) {
    if (!response.body || !response.headers.get('content-type')?.includes('text/event-stream')) return null;
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    let finalData = null;
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const chunks = buffer.split('\n\n');
      buffer = chunks.pop() || '';
      for (const chunk of chunks) {
        const lines = chunk.split('\n');
        const event = (lines.find(line => line.startsWith('event:')) || 'event: message').replace(/^event:\s*/, '').trim();
        const dataLine = lines.find(line => line.startsWith('data:'));
        const payload = dataLine ? JSON.parse(dataLine.replace(/^data:\s*/, '')) : {};
        if (event === 'status') {
          if (payload.stage === 'delta') {
            appendStreamDelta(payload.delta || '');
          } else {
            if (payload.stage === 'tool') pushToolCallStart(payload);
            if (payload.stage === 'tool_done') applyToolCallDone(payload);
          }
        }
        if (event === 'final') finalData = payload;
        if (event === 'error') throw new Error(payload.detail || 'AI 助理暫時無法處理');
      }
    }
    return finalData;
  }

  function inlineAssistantText(text, sources = []) {
    const parts = [];
    const pattern = /(\*\*[^*]+\*\*|\[[1-9][0-9]*\])/g;
    let last = 0;
    String(text || '').replace(pattern, (match, _token, offset) => {
      if (offset > last) parts.push(String(text).slice(last, offset));
      if (match.startsWith('**')) {
        parts.push(<strong key={`b-${offset}`} style={{ color: TABI_COLORS.ink, fontWeight: 950 }}>{match.slice(2, -2)}</strong>);
      } else {
        const number = parseInt(match.slice(1, -1), 10);
        const source = sources[number - 1];
        if (source?.url) {
          parts.push(
            <a key={`c-${offset}`} href={source.url} target="_blank" rel="noreferrer" title={source.title || source.url} style={{
              display: 'inline-flex',
              alignItems: 'center',
              justifyContent: 'center',
              minWidth: 18,
              height: 18,
              margin: '0 2px',
              borderRadius: 999,
              background: `${TABI_COLORS.red}14`,
              color: TABI_COLORS.red,
              fontSize: 11,
              lineHeight: '18px',
              fontWeight: 950,
              textDecoration: 'none',
              verticalAlign: '0.08em',
            }}>{number}</a>
          );
        } else {
          parts.push(match);
        }
      }
      last = offset + match.length;
      return match;
    });
    if (last < String(text || '').length) parts.push(String(text || '').slice(last));
    return parts;
  }

  function renderAssistantMarkdown(text, sources = []) {
    const normalized = String(text || '').replace(/\r\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
    if (!normalized) return null;
    const blocks = normalized.split(/\n\s*\n/);
    return (
      <div style={{
        display: 'grid',
        gap: 8,
        whiteSpace: 'normal',
        wordBreak: 'break-word',
        overflowWrap: 'anywhere',
      }}>
        {blocks.map((block, blockIdx) => {
          const lines = block.split('\n').map(line => line.trim()).filter(Boolean);
          if (!lines.length) return null;
          const heading = lines.length === 1 ? lines[0].match(/^#{1,4}\s*(.+)$/) : null;
          if (heading) {
            return (
              <div key={`h-${blockIdx}`} style={{
                marginTop: blockIdx ? 2 : 0,
                fontFamily: tabiSerif,
                fontSize: 16,
                lineHeight: 1.32,
                color: TABI_COLORS.ink,
                fontWeight: 850,
              }}>{inlineAssistantText(heading[1], sources)}</div>
            );
          }
          const listLines = lines.filter(line => /^[-*]\s+/.test(line) || /^[0-9]+[.)]\s+/.test(line));
          if (listLines.length === lines.length) {
            return (
              <div key={`ul-${blockIdx}`} style={{ display: 'grid', gap: 5 }}>
                {lines.map((line, idx) => (
                  <div key={`${blockIdx}-${idx}`} style={{
                    display: 'grid',
                    gridTemplateColumns: '16px minmax(0, 1fr)',
                    gap: 5,
                    alignItems: 'start',
                  }}>
                    <span style={{ color: TABI_COLORS.red, fontWeight: 950 }}>{/^[0-9]+/.test(line) ? line.match(/^[0-9]+/)?.[0] : '•'}</span>
                    <span>{inlineAssistantText(line.replace(/^[-*]\s+/, '').replace(/^[0-9]+[.)]\s+/, ''), sources)}</span>
                  </div>
                ))}
              </div>
            );
          }
          return (
            <div key={`p-${blockIdx}`} style={{ lineHeight: 1.68 }}>
              {lines.map((line, idx) => (
                <React.Fragment key={`${blockIdx}-${idx}`}>
                  {idx > 0 && <br />}
                  {inlineAssistantText(line.replace(/^#{1,4}\s*/, ''), sources)}
                </React.Fragment>
              ))}
            </div>
          );
        })}
      </div>
    );
  }

  function renderAssistantFlexCards(cards = []) {
    if (!Array.isArray(cards) || cards.length === 0) return null;
    return (
      <div style={{
        marginTop: 10,
        display: 'flex',
        gap: 10,
        maxWidth: '100%',
        boxSizing: 'border-box',
        overflowX: 'auto',
        padding: '2px 1px 8px',
        scrollSnapType: 'x mandatory',
        WebkitOverflowScrolling: 'touch',
      }}>
        {cards.slice(0, 6).map((card, idx) => {
          const action = (card.actions || []).find(a => a.type === 'link' && a.url);
          return (
            <div key={card.id || `${card.title}-${idx}`} style={{
              flex: '0 0 min(246px, 82vw)',
              scrollSnapAlign: 'start',
              border: `1px solid ${TABI_COLORS.line}`,
              background: TABI_COLORS.card,
              borderRadius: 16,
              overflow: 'hidden',
              boxShadow: '0 12px 26px rgba(70,55,38,0.09)',
            }}>
              {card.image_url ? (
                <img src={card.image_url} alt="" loading="lazy" style={{
                  width: '100%',
                  height: 108,
                  objectFit: 'cover',
                  display: 'block',
                  background: TABI_COLORS.bg,
                }} onError={e => { e.currentTarget.style.display = 'none'; }} />
              ) : (
                <div style={{
                  height: 82,
                  background: `linear-gradient(135deg, ${TABI_COLORS.ink} 0%, #463326 58%, ${TABI_COLORS.red} 100%)`,
                  color: '#FFFAF2',
                  display: 'flex',
                  alignItems: 'flex-end',
                  justifyContent: 'space-between',
                  padding: 12,
                }}>
                  <div style={{ fontSize: 10.5, letterSpacing: '0.12em', fontWeight: 950, color: 'rgba(255,250,242,0.62)' }}>
                    SOURCE
                  </div>
                  <div style={{
                    width: 34,
                    height: 34,
                    borderRadius: 999,
                    display: 'grid',
                    placeItems: 'center',
                    background: 'rgba(255,250,242,0.16)',
                    border: '1px solid rgba(255,250,242,0.22)',
                    fontWeight: 950,
                  }}>{card.source_number || idx + 1}</div>
                </div>
              )}
              <div style={{ padding: 12, minWidth: 0 }}>
                <div style={{ display: 'flex', gap: 6, alignItems: 'center', marginBottom: 7, minWidth: 0 }}>
                  {(card.badges || []).slice(0, 2).map((badge, badgeIdx) => (
                    <span key={`${badge.label}-${badgeIdx}`} style={{
                      flex: '0 0 auto',
                      borderRadius: 999,
                      padding: '3px 7px',
                      background: `${TABI_COLORS.red}12`,
                      color: TABI_COLORS.red,
                      fontSize: 10.5,
                      fontWeight: 950,
                    }}>{badge.label}</span>
                  ))}
                  <span style={{
                    minWidth: 0,
                    color: TABI_COLORS.ink3,
                    fontSize: 10.5,
                    fontWeight: 800,
                    overflow: 'hidden',
                    textOverflow: 'ellipsis',
                    whiteSpace: 'nowrap',
                  }}>{card.source_label || card.subtitle}</span>
                </div>
                <div style={{
                  fontSize: 13.5,
                  lineHeight: 1.35,
                  color: TABI_COLORS.ink,
                  fontWeight: 950,
                  display: '-webkit-box',
                  WebkitLineClamp: 2,
                  WebkitBoxOrient: 'vertical',
                  overflow: 'hidden',
                }}>{card.title || '搜尋結果'}</div>
                <div style={{
                  marginTop: 7,
                  minHeight: 42,
                  fontSize: 12,
                  lineHeight: 1.45,
                  color: TABI_COLORS.ink3,
                  display: '-webkit-box',
                  WebkitLineClamp: 2,
                  WebkitBoxOrient: 'vertical',
                  overflow: 'hidden',
                }}>{card.body || card.subtitle || ''}</div>
                {action && (
                  <a href={action.url} target="_blank" rel="noreferrer" style={{
                    marginTop: 10,
                    minHeight: 34,
                    borderRadius: 12,
                    display: 'grid',
                    placeItems: 'center',
                    background: TABI_COLORS.ink,
                    color: '#fff',
                    textDecoration: 'none',
                    fontSize: 12.5,
                    fontWeight: 950,
                  }}>{action.label || '開啟'}</a>
                )}
              </div>
            </div>
          );
        })}
      </div>
    );
  }

  function toggleToolCall(id) {
    setExpandedToolCalls(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  }

  function renderToolCallCard(call) {
    const expanded = expandedToolCalls.has(call.id);
    const isDone = call.status === 'done';
    const hasBody = (call.input && Object.keys(call.input).length > 0) || call.output;
    return (
      <div key={call.id} style={{
        width: '100%',
        boxSizing: 'border-box',
        borderRadius: 14,
        border: `1px solid ${TABI_COLORS.line}`,
        background: 'rgba(255,250,242,0.9)',
        boxShadow: TABI_COLORS.shadowSoft,
        overflow: 'hidden',
      }}>
        <button
          onClick={() => hasBody && toggleToolCall(call.id)}
          style={{
            width: '100%',
            display: 'flex',
            alignItems: 'center',
            gap: 8,
            padding: '9px 12px',
            background: 'transparent',
            border: 'none',
            cursor: hasBody ? 'pointer' : 'default',
            textAlign: 'left',
          }}
        >
          <span style={{
            width: 26, height: 26, borderRadius: 9, display: 'grid', placeItems: 'center',
            background: `${TABI_COLORS.red}14`, color: TABI_COLORS.red, fontSize: 13, flex: '0 0 auto',
          }}>🔧</span>
          <span style={{ flex: 1, minWidth: 0, fontSize: 12.5, fontWeight: 900, color: TABI_COLORS.ink, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {call.label}
          </span>
          <span style={{
            flex: '0 0 auto',
            fontSize: 10.5,
            fontWeight: 950,
            padding: '3px 8px',
            borderRadius: 999,
            background: call.status === 'error' ? `${TABI_COLORS.redDeep}1c` : isDone ? `${TABI_COLORS.green}1c` : `${TABI_COLORS.warn}1c`,
            color: call.status === 'error' ? TABI_COLORS.redDeep : isDone ? TABI_COLORS.green : TABI_COLORS.warn,
          }}>{call.status === 'error' ? '失敗' : isDone ? '完成' : '執行中'}</span>
          {hasBody && (
            <span style={{
              flex: '0 0 auto', color: TABI_COLORS.ink3, fontSize: 11,
              transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform .15s',
            }}>▾</span>
          )}
        </button>
        {expanded && (
          <div style={{ padding: '0 12px 12px', display: 'grid', gap: 8 }}>
            {call.input && Object.keys(call.input).length > 0 && (
              <div>
                <div style={{ fontSize: 10.5, color: TABI_COLORS.ink3, fontWeight: 900, letterSpacing: '0.08em', marginBottom: 4 }}>輸入</div>
                <pre style={{
                  margin: 0,
                  fontFamily: 'ui-monospace,SFMono-Regular,Menlo,monospace',
                  fontSize: 11,
                  lineHeight: 1.5,
                  color: TABI_COLORS.ink2,
                  background: TABI_COLORS.bg,
                  border: `1px solid ${TABI_COLORS.line}`,
                  borderRadius: 10,
                  padding: '8px 10px',
                  overflowX: 'auto',
                  whiteSpace: 'pre-wrap',
                  wordBreak: 'break-word',
                }}>{JSON.stringify(call.input, null, 2)}</pre>
              </div>
            )}
            {call.output && (
              <div>
                <div style={{ fontSize: 10.5, color: TABI_COLORS.ink3, fontWeight: 900, letterSpacing: '0.08em', marginBottom: 4 }}>結果</div>
                <div style={{
                  background: TABI_COLORS.bg,
                  border: `1px solid ${TABI_COLORS.line}`,
                  borderRadius: 10,
                  padding: '8px 10px',
                  display: 'grid',
                  gap: 6,
                }}>
                  {call.output.message && (
                    <div style={{ fontSize: 12, color: TABI_COLORS.ink2, lineHeight: 1.5 }}>{call.output.message}</div>
                  )}
                  {(call.output.sources || []).map((s, idx) => (
                    <div key={`s-${idx}`} style={{ display: 'grid', gap: 2 }}>
                      {s.url ? (
                        <a href={s.url} target="_blank" rel="noreferrer" style={{
                          fontSize: 11.5, color: TABI_COLORS.blue, textDecoration: 'none',
                          overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block',
                        }}>{idx + 1}. {s.title || s.url}</a>
                      ) : (
                        <div style={{ fontSize: 11.5, color: TABI_COLORS.ink2 }}>{idx + 1}. {s.title}</div>
                      )}
                      {s.content && (
                        <div style={{ fontSize: 11, color: TABI_COLORS.ink3, lineHeight: 1.45 }}>{s.content}</div>
                      )}
                    </div>
                  ))}
                  {(call.output.rows || []).map((row, idx) => (
                    <div key={`${row.label}-${idx}`} style={{ fontSize: 11.5, color: TABI_COLORS.ink2, display: 'flex', justifyContent: 'space-between', gap: 8 }}>
                      <span style={{ color: TABI_COLORS.ink3 }}>{row.label}</span>
                      <span style={{ fontWeight: 850, textAlign: 'right' }}>{row.value}</span>
                    </div>
                  ))}
                </div>
              </div>
            )}
          </div>
        )}
      </div>
    );
  }

  function applyDraftInstruction(text) {
    if (!draftPayload) return false;
    const patch = {};
    let touched = false;
    const value = normalizeAssistantText(text);
    if (/共同|大家|分帳/.test(value)) {
      patch.visibility = 'shared';
      patch.split_method = /自訂/.test(value) ? 'custom' : (draftPayload.split_method === 'custom' ? 'custom' : 'equal');
      patch.is_proxy = false;
      patch.proxy_for = null;
      touched = true;
    }
    if (/個人|自己的|私人/.test(value)) {
      patch.visibility = 'personal';
      patch.split_method = 'solo';
      patch.splits = null;
      touched = true;
    }
    if (/均分/.test(value)) {
      patch.visibility = 'shared';
      patch.split_method = 'equal';
      patch.splits = null;
      touched = true;
    }
    if (/自訂/.test(value)) {
      patch.visibility = 'shared';
      patch.split_method = 'custom';
      patch.splits = members.reduce((acc, m) => {
        acc[m.id] = (draftPayload.splits || {})[m.id] || 0;
        return acc;
      }, {});
      touched = true;
    }
    const amountMatch = value.match(/(?:金額|改|換|變成|是)?\s*([0-9][0-9,]*)\s*(日圓|円|yen|jpy|台幣|twd|ntd)?/i);
    if (amountMatch) {
      patch.amount = parseInt(amountMatch[1].replace(/,/g, ''), 10) || draftPayload.amount;
      const word = (amountMatch[2] || '').toLowerCase();
      if (['台幣', 'twd', 'ntd'].includes(word)) patch.currency = 'TWD';
      if (['日圓', '円', 'yen', 'jpy'].includes(word)) patch.currency = 'JPY';
      touched = true;
    }
    for (const c of ['餐飲', '交通', '購物', '住宿', '門票', '其他']) {
      if (value.includes(c)) {
        patch.category = c;
        touched = true;
        break;
      }
    }
    const titleMatch = value.match(/(?:名稱|品項|標題)(?:改成|改|是|：|:)\s*([^，,。]+)/);
    if (titleMatch) {
      patch.title = titleMatch[1].trim().slice(0, 40);
      touched = true;
    }
    if (!touched) return false;
    setDraftPayload(prev => prev ? { ...prev, ...patch } : prev);
    setDraftExpanded(false);
    setMessages(prev => [...prev, { role: 'assistant', text: '我已更新草稿，先幫你收成小卡片。' }]);
    return true;
  }

  async function sendMessage(overrideText) {
    const text = normalizeAssistantText((overrideText ?? input).trim());
    if (!text || busy) return;
    setInput('');
    setMessages(prev => [...prev, { role: 'user', text }]);
    if (draftPayload && applyDraftInstruction(text)) return;
    setDraft(null);
    setDraftPayload(null);
    setDraftExpanded(false);
    setSavedExpenseId(null);
    setToolCalls([]);
    toolCallsRef.current = [];
    streamingTextRef.current = '';
    setStreamingText('');
    setExpandedToolCalls(new Set());
    setBusy(true);
    try {
      const cid = conversationId || await createConversation(tripId || currentTripId || null);
      let locationPayload = null;
      if (shouldUseLocation(text)) {
        const locationResult = await requestCurrentLocation();
        locationPayload = locationResult.location;
        if (!locationPayload) {
          setMessages(prev => [...prev, {
            role: 'assistant',
            text: locationHelpMessage(locationResult.reason),
            result: {
              kind: 'location_required',
              title: '需要開啟定位',
              rows: [
                { label: '目前狀態', value: locationResult.reason === 'denied' ? '定位被封鎖' : '未取得定位' },
                { label: '替代方式', value: '輸入明確地點搜尋' },
              ],
            },
          }]);
          return;
        }
      }
      const requestPayload = { text, trip_id: tripId || null, location: locationPayload };
      const streamResponse = await fetch(`/api/assistant/conversations/${cid}/messages/stream`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}`, ...window.tabiLLMHeaders() },
        body: JSON.stringify(requestPayload),
      });
      let data = await readAssistantStream(streamResponse);
      if (!streamResponse.ok && !data) {
        const errorData = await streamResponse.json().catch(() => ({}));
        throw new Error(errorData.detail || 'AI 助理暫時無法處理');
      }
      if (!data) {
        const r = await fetch(`/api/assistant/conversations/${cid}/messages`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}`, ...window.tabiLLMHeaders() },
          body: JSON.stringify(requestPayload),
        });
        data = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(data.detail || 'AI 助理暫時無法處理');
      }
      applyAssistantResponse(data);
    } catch (e) {
      const erroredToolCalls = toolCallsRef.current.map(c => c.status === 'running' ? { ...c, status: 'error' } : c);
      toolCallsRef.current = [];
      setToolCalls([]);
      streamingTextRef.current = '';
      setStreamingText('');
      setMessages(prev => [...prev, {
        role: 'assistant',
        text: e.message || '解析失敗，請換個說法。',
        ...(erroredToolCalls.length > 0 ? { toolCalls: erroredToolCalls } : {}),
      }]);
    } finally {
      setBusy(false);
    }
  }

  async function confirmDraft() {
    if (!draft?.trip?.id || !draftPayload || busy) return;
    setBusy(true);
    try {
      const url = savedExpenseId
        ? `/api/trips/${draft.trip.id}/expenses/${savedExpenseId}`
        : `/api/trips/${draft.trip.id}/expenses`;
      const r = await fetch(url, {
        method: savedExpenseId ? 'PATCH' : 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
        body: JSON.stringify(draftPayload),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(data.detail || (savedExpenseId ? '更新失敗' : '新增失敗'));
      const nextId = data.id || savedExpenseId;
      setBusy(false);
      setSavedExpenseId(nextId);
      setMessages(prev => [...prev, { role: 'assistant', text: savedExpenseId ? '已更新這筆支出。' : `已新增到「${draft.trip.name}」。` }]);
      setDraftExpanded(false);
      if (go) go('trips');
    } catch (e) {
      setMessages(prev => [...prev, { role: 'assistant', text: e.message || '新增失敗，請再確認一次。' }]);
    } finally {
      setBusy(false);
    }
  }

  const trip = selectedTrip();
  const members = draft?.trip?.members || trip?.members || [];
  const splitTotal = draftPayload?.split_method === 'custom'
    ? Object.values(draftPayload.splits || {}).reduce((sum, v) => sum + (parseInt(v, 10) || 0), 0)
    : 0;
  const warningParts = [
    ...(draftPayload && Number(draftPayload.amount || 0) <= 0 ? ['請填入有效金額。'] : []),
    ...(draftPayload?.visibility === 'shared' && draftPayload?.split_method === 'custom' && splitTotal !== Number(draftPayload.amount || 0)
      ? [`自訂分帳加總 ${splitTotal.toLocaleString()}，需等於總額 ${Number(draftPayload.amount || 0).toLocaleString()}。`]
      : []),
  ];
  const warning = Array.from(new Set(warningParts)).join(' ');

  const fieldStyle = {
    width: '100%', minHeight: 39, borderRadius: 12, border: `1px solid ${TABI_COLORS.line}`,
    background: TABI_COLORS.bg, padding: '0 10px', color: TABI_COLORS.ink,
    fontSize: 16, fontWeight: 700, boxSizing: 'border-box',
  };
  const labelStyle = { display: 'block', fontSize: 10.5, fontWeight: 900, color: TABI_COLORS.ink3, marginBottom: 5 };
  const patchPayload = (patch) => setDraftPayload(prev => prev ? { ...prev, ...patch } : prev);
  const setVisibility = (visibility) => {
    setDraftPayload(prev => {
      if (!prev) return prev;
      return {
        ...prev,
        visibility,
        split_method: visibility === 'personal' ? 'solo' : (prev.split_method === 'custom' ? 'custom' : 'equal'),
        splits: visibility === 'personal' ? null : prev.splits,
        is_proxy: visibility === 'personal' ? prev.is_proxy : false,
        proxy_for: visibility === 'personal' ? prev.proxy_for : null,
      };
    });
  };
  const setSplitMethod = (split_method) => {
    setDraftPayload(prev => {
      if (!prev) return prev;
      const next = { ...prev, split_method };
      if (split_method === 'custom') {
        const current = prev.splits || {};
        next.splits = members.reduce((acc, m) => {
          acc[m.id] = current[m.id] || 0;
          return acc;
        }, {});
      } else {
        next.splits = null;
      }
      return next;
    });
  };
  const setSplitAmount = (memberId, value) => {
    const amount = Math.max(0, parseInt(String(value || '').replace(/,/g, ''), 10) || 0);
    setDraftPayload(prev => prev ? { ...prev, splits: { ...(prev.splits || {}), [memberId]: amount } } : prev);
  };
  const startVoiceInput = () => {
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SpeechRecognition) {
      setMessages(prev => [...prev, { role: 'assistant', text: '這個瀏覽器目前不支援語音輸入。' }]);
      return;
    }
    if (listening && recognitionRef.current) {
      recognitionRef.current.stop();
      return;
    }
    const rec = new SpeechRecognition();
    recognitionRef.current = rec;
    rec.lang = 'zh-TW';
    rec.interimResults = false;
    rec.continuous = false;
    rec.onstart = () => setListening(true);
    rec.onend = () => setListening(false);
    rec.onerror = () => setListening(false);
    rec.onresult = (event) => {
      const transcript = Array.from(event.results || []).map(r => r[0]?.transcript || '').join('').trim();
      if (transcript) setInput(prev => `${prev}${prev ? ' ' : ''}${transcript}`);
    };
    rec.start();
  };
  const handleImagePick = async (file) => {
    if (!file) return;
    const imageUrl = URL.createObjectURL(file);
    setMessages(prev => [
      ...prev,
      { role: 'user', text: `上傳圖片：${file.name}`, imageUrl },
    ]);
    setBusy(true);
    try {
      const cid = conversationId || await createConversation(tripId || currentTripId || null);
      const form = new FormData();
      form.append('image', file);
      form.append('text', input.trim() || '分析這張圖片，如果是商品請整理資訊');
      if (tripId) form.append('trip_id', tripId);
      if (cid) form.append('conversation_id', cid);
      const r = await fetch('/api/assistant/image', {
        method: 'POST',
        headers: { Authorization: `Bearer ${window.tabiToken()}`, ...window.tabiLLMHeaders() },
        body: form,
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(data.detail || '圖片分析失敗');
      setMessages(prev => [...prev, { role: 'assistant', text: data.message || '我分析了這張圖片。', result: data.result }]);
    } catch (e) {
      setMessages(prev => [...prev, { role: 'assistant', text: e.message || '圖片分析失敗。' }]);
    } finally {
      setBusy(false);
    }
  };
  const moneyText = (amount, currency) => {
    if (currency === 'JPY') return `¥${Number(amount || 0).toLocaleString()}`;
    if (currency === 'TWD') return `NT$${Number(amount || 0).toLocaleString()}`;
    return `${currency || ''} ${Number(amount || 0).toLocaleString()}`.trim();
  };
  const copyAssistantText = async (text) => {
    await navigator.clipboard?.writeText(text || '');
  };
  const shareAssistantInvite = (invite) => {
    if (!invite) return;
    if (navigator.share) {
      navigator.share({
        title: `加入 ${invite.trip_name} 的旅行記帳`,
        text: invite.share_text,
        url: invite.url,
      });
    } else {
      copyAssistantText(invite.share_text || invite.url);
    }
  };
  const openArtifact = (artifact) => {
    if (!artifact?.url) return;
    window.open(artifact.url, '_blank', 'noopener,noreferrer');
  };
  const moneyListText = (rows, fallbackAmount, fallbackCurrency) => {
    if (Array.isArray(rows) && rows.length) {
      return rows.map(row => moneyText(row.amount, row.currency)).join(' / ');
    }
    return moneyText(fallbackAmount, fallbackCurrency);
  };
  const dateValue = draftPayload?.expense_date ? String(draftPayload.expense_date).slice(0, 10) : '';

  return (
    <>
      {!open && (
        <button
          onPointerDown={onPointerDown}
          onPointerMove={onPointerMove}
          onPointerUp={onPointerUp}
          onPointerCancel={onPointerCancel}
          style={{
            position: 'fixed', right: pos.right, bottom: pos.bottom, zIndex: 8000,
            width: 72, height: 72, borderRadius: 0, border: 'none',
            background: 'transparent',
            color: '#fff', fontWeight: 900, fontSize: 15, cursor: dragging ? 'grabbing' : 'grab',
            boxShadow: 'none',
            touchAction: 'none',
            padding: 0,
            overflow: 'visible',
            userSelect: 'none',
            WebkitUserSelect: 'none',
            transition: dragging ? 'none' : 'transform 0.12s ease',
          }}
          aria-label="AI 助理"
        ><img draggable="false" src="/assistant-icon.png" alt="" style={{
          width: '100%', height: '100%', objectFit: 'contain', display: 'block',
          transform: 'scale(1.18)',
          transformOrigin: 'center',
          pointerEvents: 'none', userSelect: 'none', WebkitUserDrag: 'none',
        }} /></button>
      )}
      {open && ReactDOM.createPortal(
        <div style={{
          position: 'fixed', inset: 0, zIndex: 9000,
          background: 'rgba(24,20,15,0.34)',
          display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
        }}>
          <div style={{
            width: '100%', maxWidth: 520, height: 'min(100dvh, 820px)',
            borderRadius: '24px 24px 0 0',
            background: TABI_COLORS.bg,
            boxShadow: '0 -24px 60px rgba(24,20,15,0.24)',
            display: 'flex', flexDirection: 'column', overflow: 'hidden', position: 'relative',
          }}>
            <div style={{
              padding: '14px 16px 12px', borderBottom: `1px solid ${TABI_COLORS.line}`,
              display: 'flex', alignItems: 'center', gap: 10,
              background: 'rgba(255,250,242,0.96)',
            }}>
              <img src="/assistant-icon.png" alt="" style={{
                width: 44, height: 44, objectFit: 'contain',
                flexShrink: 0,
              }} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontFamily: tabiSerif, fontSize: 19, fontWeight: 800, color: TABI_COLORS.ink }}>旅程助理</div>
                <div style={{ fontSize: 11.5, color: TABI_COLORS.ink3, marginTop: 2 }}>自然語言操作你的旅行工具</div>
              </div>
              <button onClick={() => createConversation(tripId || currentTripId || null)} title="新對話" style={{
                width: 38, height: 38, borderRadius: 14, border: `1px solid ${TABI_COLORS.line}`,
                background: TABI_COLORS.card, cursor: 'pointer', color: TABI_COLORS.ink, fontSize: 22,
                display: 'grid', placeItems: 'center',
              }}>+</button>
              <button onClick={() => setShowHistory(v => !v)} title="歷史對話" style={{
                width: 38, height: 38, borderRadius: 14, border: `1px solid ${showHistory ? TABI_COLORS.red : TABI_COLORS.line}`,
                background: showHistory ? `${TABI_COLORS.red}12` : TABI_COLORS.card,
                cursor: 'pointer', color: showHistory ? TABI_COLORS.red : TABI_COLORS.ink, fontSize: 17,
                display: 'grid', placeItems: 'center',
              }}>≡</button>
              <button onClick={() => setOpen(false)} style={{
                width: 38, height: 38, borderRadius: 14, border: `1px solid ${TABI_COLORS.line}`,
                background: TABI_COLORS.card, cursor: 'pointer', color: TABI_COLORS.ink, fontSize: 20,
              }}>×</button>
            </div>

            <div style={{ padding: '10px 14px 12px', borderBottom: `1px solid ${TABI_COLORS.line}`, background: 'rgba(247,240,229,0.74)' }}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 7 }}>
                <label style={{ display: 'block', fontSize: 10.5, fontWeight: 900, color: TABI_COLORS.ink3, letterSpacing: '0.08em' }}>工作旅程</label>
                <span style={{ fontSize: 11, color: TABI_COLORS.ink3, fontWeight: 800 }}>{trip?.is_active ? '進行中' : '可切換'}</span>
              </div>
              <select value={tripId} onChange={e => {
                const nextTripId = e.target.value;
                setTripId(nextTripId);
                localStorage.setItem('tabi_assistant_trip_id', nextTripId);
                if (conversationId) {
                  fetch(`/api/assistant/conversations/${conversationId}`, {
                    method: 'PATCH',
                    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${window.tabiToken()}` },
                    body: JSON.stringify({ default_trip_id: nextTripId || null }),
                  }).catch(() => {});
                }
              }} style={{
                width: '100%', height: 44, borderRadius: 16, border: `1px solid ${TABI_COLORS.line}`,
                background: TABI_COLORS.card, padding: '0 13px', color: TABI_COLORS.ink, fontWeight: 850,
                fontSize: 16, boxSizing: 'border-box',
              }}>
                {(context.trips || []).map(t => (
                  <option key={t.id} value={t.id}>{t.name}{t.is_active ? '（進行中）' : ''}</option>
                ))}
              </select>
            </div>

            <div ref={boxRef} style={{
              flex: 1, overflowY: 'auto', padding: '14px 14px 18px',
              display: 'flex', flexDirection: 'column', gap: 10,
              background: 'linear-gradient(180deg, rgba(247,240,229,0.62), rgba(250,246,240,1) 42%)',
            }}>
              {!draftExpanded && messages.length === 0 && !draft && (
                <>
                  <div style={{
                    padding: '4px 3px 5px',
                  }}>
                    <div style={{
                      fontFamily: tabiSerif,
                      fontSize: 22,
                      lineHeight: 1.25,
                      color: TABI_COLORS.ink,
                      fontWeight: 850,
                    }}>
                      {trip?.name ? `${greetingWord}！「${trip.name}」想先做點什麼？` : `${greetingWord}！想先處理哪趟旅程？`}
                    </div>
                    <div style={{
                      marginTop: 10,
                      border: `1px solid ${TABI_COLORS.line}`,
                      borderRadius: 18,
                      background: 'rgba(255,250,242,0.9)',
                      padding: '13px 14px',
                      boxShadow: '0 10px 24px rgba(55,42,31,0.05)',
                    }}>
                      <div style={{ fontSize: 13, lineHeight: 1.65, color: TABI_COLORS.ink2, fontWeight: 650 }}>
                        我可以用自然語言幫你操作旅程：查地點、整理購物清單、更新行前準備、產生支出統計，也可以把你輸入的支出整理成可確認的草稿。
                      </div>
                      <div style={{ marginTop: 10, display: 'flex', flexWrap: 'wrap', gap: 7 }}>
                        {assistantExamples.map(example => (
                          <button key={example} onClick={() => setInput(example)} style={{
                            border: `1px solid ${TABI_COLORS.line}`,
                            background: 'rgba(255,255,255,0.58)',
                            color: TABI_COLORS.ink2,
                            borderRadius: 999,
                            padding: '7px 10px',
                            fontSize: 11.5,
                            lineHeight: 1.2,
                            cursor: 'pointer',
                            maxWidth: '100%',
                          }}>
                            {example}
                          </button>
                        ))}
                      </div>
                    </div>
                  </div>
                  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 8 }}>
                    {quickActions.map(a => (
                      <button key={a.label} onClick={() => useQuickAction(a.text)} style={{
                        textAlign: 'center',
                        border: `1px solid ${TABI_COLORS.line}`,
                        background: 'rgba(255,250,242,0.92)',
                        borderRadius: 15,
                        padding: '10px 6px',
                        color: TABI_COLORS.ink,
                        cursor: 'pointer',
                        display: 'flex',
                        flexDirection: 'column',
                        alignItems: 'center',
                        gap: 6,
                        minWidth: 0,
                      }}>
                        <span style={{
                          width: 30,
                          height: 30,
                          borderRadius: 11,
                          display: 'grid',
                          placeItems: 'center',
                          background: `${a.color}14`,
                          color: a.color,
                          fontWeight: 950,
                          fontSize: 14,
                        }}>{a.icon}</span>
                        <span style={{ minWidth: 0 }}>
                          <span style={{ display: 'block', fontSize: 11.5, fontWeight: 950, color: TABI_COLORS.ink, whiteSpace: 'nowrap' }}>{a.label}</span>
                        </span>
                      </button>
                    ))}
                  </div>
                </>
              )}
              {!draftExpanded && messages.map((m, i) => (
                <React.Fragment key={i}>
                <div style={{
                  alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start',
                  width: 'fit-content',
                  maxWidth: m.role === 'user' ? '78%' : '92%',
                  boxSizing: 'border-box',
                  padding: '10px 12px',
                  borderRadius: m.role === 'user' ? '16px 16px 4px 16px' : '16px 16px 16px 4px',
                  background: m.role === 'user' ? TABI_COLORS.red : TABI_COLORS.card,
                  color: m.role === 'user' ? '#fff' : TABI_COLORS.ink2,
                  fontSize: 13, lineHeight: 1.55, boxShadow: m.role === 'user' ? 'none' : TABI_COLORS.shadowSoft,
                  minWidth: 0,
                  overflow: 'visible',
                  overflowWrap: 'anywhere',
                  wordBreak: 'break-word',
                  whiteSpace: 'normal',
                }}>
                  {m.role === 'assistant' && m.toolCalls?.length > 0 && (
                    <div style={{ display: 'grid', gap: 6, marginBottom: 8 }}>
                      {m.toolCalls.map(call => renderToolCallCard(call))}
                    </div>
                  )}
                  {m.imageUrl && <img src={m.imageUrl} alt="" style={{ width: 138, maxWidth: '100%', borderRadius: 12, display: 'block', marginBottom: 8 }} />}
                  <div style={{
                    maxWidth: '100%',
                    minWidth: 0,
                    whiteSpace: 'normal',
                    overflowWrap: 'anywhere',
                    wordBreak: 'break-word',
                  }}>
                    {m.role === 'assistant' ? renderAssistantMarkdown(m.text, m.result?.sources || []) : m.text}
                  </div>
                  {m.draft && (
                    <div style={{
                      marginTop: 10,
                      background: TABI_COLORS.bg,
                      border: `1px solid ${TABI_COLORS.line}`,
                      borderRadius: 14,
                      padding: 10,
                    }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
                        <div style={{
                          width: 34, height: 34, borderRadius: 12, display: 'grid', placeItems: 'center',
                          background: m.draft.payload?.visibility === 'shared' ? `${TABI_COLORS.warn}22` : `${TABI_COLORS.red}18`,
                          color: m.draft.payload?.visibility === 'shared' ? TABI_COLORS.warn : TABI_COLORS.red,
                          fontWeight: 900,
                        }}>{m.draft.payload?.visibility === 'shared' ? '割' : '私'}</div>
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <div style={{ fontSize: 13, fontWeight: 950, color: TABI_COLORS.ink, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                            {m.draft.payload?.title || '支出草稿'}
                          </div>
                          <div style={{ marginTop: 3, fontSize: 12, color: TABI_COLORS.ink3, fontWeight: 800 }}>
                            {moneyText(m.draft.payload?.amount, m.draft.payload?.currency)} · {m.draft.payload?.visibility === 'shared' ? '共同支出' : '個人支出'}
                          </div>
                        </div>
                      </div>
                      <button onClick={() => {
                        setDraft(m.draft);
                        setDraftPayload(m.draft.payload || null);
                        setDraftExpanded(true);
                        setSavedExpenseId(null);
                        if (m.draft.trip?.id) {
                          setTripId(m.draft.trip.id);
                          localStorage.setItem('tabi_assistant_trip_id', m.draft.trip.id);
                        }
                      }} style={{
                        width: '100%', marginTop: 10, minHeight: 34, borderRadius: 11, border: 'none',
                        background: TABI_COLORS.ink, color: '#fff', fontWeight: 850, cursor: 'pointer',
                      }}>開啟編輯</button>
                    </div>
                  )}
                  {m.stats && (
                    <div style={{ marginTop: 10, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 7 }}>
                      {[
                        ['總支出', moneyText(m.stats.total, m.stats.currency)],
                        ['我的合計', moneyText(m.stats.my_total, m.stats.currency)],
                        ['個人', moneyText(m.stats.personal_total, m.stats.currency)],
                        ['共同', moneyText(m.stats.shared_total, m.stats.currency)],
                      ].map(([label, value]) => (
                        <div key={label} style={{
                          background: TABI_COLORS.bg,
                          border: `1px solid ${TABI_COLORS.line}`,
                          borderRadius: 12,
                          padding: '8px 9px',
                        }}>
                          <div style={{ fontSize: 10.5, color: TABI_COLORS.ink3, fontWeight: 850 }}>{label}</div>
                          <div style={{ marginTop: 3, color: TABI_COLORS.ink, fontWeight: 950 }}>{value}</div>
                        </div>
                      ))}
                    </div>
                  )}
                  {m.invite && (
                    <div style={{
                      marginTop: 10,
                      background: TABI_COLORS.bg,
                      border: `1px solid ${TABI_COLORS.line}`,
                      borderRadius: 14,
                      padding: 10,
                    }}>
                      <div style={{ fontSize: 12, fontWeight: 900, color: TABI_COLORS.ink }}>邀請連結</div>
                      <div style={{ marginTop: 6, fontSize: 12, lineHeight: 1.5, color: TABI_COLORS.ink3, wordBreak: 'break-all' }}>{m.invite.url}</div>
                      <div style={{ display: 'flex', gap: 7, marginTop: 10 }}>
                        <button onClick={() => copyAssistantText(m.invite.share_text || m.invite.url)} style={{
                          flex: 1, minHeight: 34, borderRadius: 11, border: 'none',
                          background: TABI_COLORS.ink, color: '#fff', fontWeight: 850, cursor: 'pointer',
                        }}>複製</button>
                        <button onClick={() => shareAssistantInvite(m.invite)} style={{
                          flex: 1, minHeight: 34, borderRadius: 11, border: `1px solid ${TABI_COLORS.line}`,
                          background: TABI_COLORS.card, color: TABI_COLORS.ink2, fontWeight: 850, cursor: 'pointer',
                        }}>分享</button>
                      </div>
                    </div>
                  )}
                  {m.artifact && (
                    <div style={{
                      marginTop: 10,
                      background: 'linear-gradient(180deg, #18140F, #2B211A)',
                      border: '1px solid rgba(255,250,242,0.12)',
                      borderRadius: 16,
                      padding: 12,
                      color: '#fff',
                      boxShadow: '0 18px 38px rgba(24,20,15,0.18)',
                    }}>
                      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
                        <div style={{
                          width: 38, height: 38, borderRadius: 13,
                          display: 'grid', placeItems: 'center',
                          background: 'rgba(255,250,242,0.12)',
                          color: '#F1D9B4', fontWeight: 950,
                        }}>表</div>
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <div style={{ fontSize: 10.5, letterSpacing: '0.12em', color: 'rgba(255,250,242,0.58)', fontWeight: 900 }}>ARTIFACT</div>
                          <div style={{ marginTop: 3, fontFamily: tabiSerif, fontSize: 17, lineHeight: 1.25, fontWeight: 850, color: '#FFFAF2' }}>
                            {m.artifact.title || '旅程支出報告'}
                          </div>
                          <div style={{ marginTop: 5, fontSize: 12, color: 'rgba(255,250,242,0.66)', lineHeight: 1.45 }}>
                            {m.artifact.summary || '已整理成手機版互動報告'}
                          </div>
                        </div>
                      </div>
                      {m.artifact.preview && (
                        <div style={{ marginTop: 12, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 7 }}>
                          <div style={{ borderRadius: 12, padding: '8px 9px', background: 'rgba(255,250,242,0.08)' }}>
                            <div style={{ fontSize: 10.5, color: 'rgba(255,250,242,0.56)', fontWeight: 850 }}>總支出</div>
                            <div style={{ marginTop: 3, color: '#FFFAF2', fontWeight: 950, lineHeight: 1.25 }}>
                              {moneyListText(m.artifact.preview.total_by_currency, m.artifact.preview.total, m.artifact.preview.currency)}
                            </div>
                          </div>
                          <div style={{ borderRadius: 12, padding: '8px 9px', background: 'rgba(255,250,242,0.08)' }}>
                            <div style={{ fontSize: 10.5, color: 'rgba(255,250,242,0.56)', fontWeight: 850 }}>筆數</div>
                            <div style={{ marginTop: 3, color: '#FFFAF2', fontWeight: 950 }}>{m.artifact.preview.expense_count || 0} 筆</div>
                          </div>
                        </div>
                      )}
                      <button onClick={() => openArtifact(m.artifact)} style={{
                        width: '100%', marginTop: 12, minHeight: 38, borderRadius: 13,
                        border: '1px solid rgba(255,250,242,0.20)',
                        background: '#FFFAF2', color: TABI_COLORS.ink,
                        fontWeight: 950, cursor: 'pointer',
                      }}>開啟完整報告</button>
                    </div>
                  )}
                  {m.result && (
                    <div style={{
                      marginTop: 10,
                      background: TABI_COLORS.bg,
                      border: `1px solid ${TABI_COLORS.line}`,
                      borderRadius: 15,
                      padding: 11,
                      maxWidth: '100%',
                      minWidth: 0,
                      overflow: 'hidden',
                    }}>
                      {m.result.image_url && <img src={m.result.image_url} alt="" style={{ width: '100%', maxHeight: 170, objectFit: 'cover', borderRadius: 12, display: 'block', marginBottom: 10 }} />}
                      <div style={{ fontFamily: tabiSerif, fontSize: 16, fontWeight: 850, color: TABI_COLORS.ink }}>{m.result.title || '操作結果'}</div>
                      {m.result.query && (
                        <div style={{ marginTop: 4, fontSize: 11.5, color: TABI_COLORS.ink3, fontWeight: 800, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                          {m.result.query}
                        </div>
                      )}
                      {m.result.cards?.length ? renderAssistantFlexCards(m.result.cards) : (
                        <div style={{ display: 'grid', gap: 8, marginTop: 9 }}>
                          {(m.result.rows || []).slice(0, 8).map((row, idx) => {
                            const clickable = !!row.action;
                            const RowTag = clickable ? 'button' : 'div';
                            return (
                            <RowTag key={`${row.label}-${idx}`} onClick={clickable ? () => runResultRowAction(row.action) : undefined} style={{
                              display: 'grid',
                              gridTemplateColumns: 'minmax(0, 1.05fr) minmax(0, 0.95fr)',
                              gap: 8,
                              alignItems: 'baseline',
                              borderBottom: idx === Math.min((m.result.rows || []).length, 8) - 1 ? 'none' : `1px solid ${TABI_COLORS.line}`,
                              paddingBottom: 7,
                              minWidth: 0,
                              width: '100%',
                              borderTop: 'none',
                              borderLeft: 'none',
                              borderRight: 'none',
                              background: clickable ? 'rgba(255,250,242,0.42)' : 'transparent',
                              cursor: clickable ? 'pointer' : 'default',
                              textAlign: 'left',
                              borderRadius: clickable ? 11 : 0,
                              padding: clickable ? '8px 9px' : '0 0 7px',
                            }}>
                              <div style={{ minWidth: 0 }}>
                                <div style={{ minWidth: 0, color: TABI_COLORS.ink2, fontSize: 12.5, fontWeight: 850, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{row.label}</div>
                                {Array.isArray(row.children) && row.children.length > 0 && (
                                  <div style={{ marginTop: 5, display: 'grid', gap: 3 }}>
                                    {row.children.slice(0, 3).map((child, childIdx) => (
                                      <div key={`${child.label}-${childIdx}`} style={{
                                        color: TABI_COLORS.ink3,
                                        fontSize: 11.5,
                                        fontWeight: 750,
                                        overflow: 'hidden',
                                        textOverflow: 'ellipsis',
                                        whiteSpace: 'nowrap',
                                      }}>{child.label} · {child.value}</div>
                                    ))}
                                  </div>
                                )}
                              </div>
                              <div style={{ minWidth: 0, color: TABI_COLORS.ink, fontSize: 12.5, fontWeight: 900, textAlign: 'right', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{row.value}{clickable ? ' ›' : ''}</div>
                            </RowTag>
                          );})}
                        </div>
                      )}
                      {!m.result.cards?.length && m.result.sources && m.result.sources.length > 0 && (
                        <div style={{ marginTop: 11, display: 'grid', gap: 7, minWidth: 0 }}>
                          <div style={{ fontSize: 10.5, color: TABI_COLORS.ink3, fontWeight: 900, letterSpacing: '0.08em' }}>來源</div>
                          {m.result.sources.slice(0, 4).map((source, idx) => (
                            <a key={`${source.url}-${idx}`} href={source.url} target="_blank" rel="noreferrer" style={{
                              display: 'block',
                              border: `1px solid ${TABI_COLORS.line}`,
                              borderRadius: 12,
                              background: TABI_COLORS.card,
                              padding: '8px 9px',
                              color: TABI_COLORS.ink,
                              textDecoration: 'none',
                              maxWidth: '100%',
                              minWidth: 0,
                              overflow: 'hidden',
                            }}>
                              <div style={{ fontSize: 12, fontWeight: 900, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                                {source.title || '未命名來源'}
                              </div>
                              <div style={{ marginTop: 3, fontSize: 10.5, color: TABI_COLORS.ink3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', direction: 'ltr' }}>
                                {(source.url || '').replace(/^https?:\/\/(www\.)?/, '')}
                              </div>
                            </a>
                          ))}
                        </div>
                      )}
                    </div>
                  )}
                </div>
                </React.Fragment>
              ))}
              {!draftExpanded && (toolCalls.length > 0 || streamingText) && (
                <div style={{
                  alignSelf: 'flex-start',
                  width: 'fit-content',
                  maxWidth: '92%',
                  boxSizing: 'border-box',
                  padding: '10px 12px',
                  borderRadius: '16px 16px 16px 4px',
                  background: TABI_COLORS.card,
                  color: TABI_COLORS.ink2,
                  fontSize: 13, lineHeight: 1.55, boxShadow: TABI_COLORS.shadowSoft,
                  minWidth: 0,
                  overflow: 'visible',
                  overflowWrap: 'anywhere',
                  wordBreak: 'break-word',
                  whiteSpace: 'normal',
                }}>
                  {toolCalls.length > 0 && (
                    <div style={{ display: 'grid', gap: 6, marginBottom: streamingText ? 8 : 0 }}>
                      {toolCalls.map(call => renderToolCallCard(call))}
                    </div>
                  )}
                  {streamingText && (
                    <div style={{
                      maxWidth: '100%',
                      minWidth: 0,
                      whiteSpace: 'normal',
                      overflowWrap: 'anywhere',
                      wordBreak: 'break-word',
                    }}>
                      {renderAssistantMarkdown(streamingText)}
                      <span style={{
                        display: 'inline-block',
                        width: 8, height: 14,
                        marginLeft: 2,
                        verticalAlign: 'text-bottom',
                        background: TABI_COLORS.ink2,
                        animation: 'tabiBlink 1s step-end infinite',
                      }} />
                    </div>
                  )}
                </div>
              )}
              {!draftExpanded && busy && toolCalls.length === 0 && !streamingText && (
                <div style={{
                  alignSelf: 'flex-start',
                  borderRadius: '16px 16px 16px 4px',
                  background: TABI_COLORS.card,
                  border: `1px solid ${TABI_COLORS.line}`,
                  boxShadow: TABI_COLORS.shadowSoft,
                  padding: '12px 14px',
                  display: 'flex',
                  alignItems: 'center',
                  gap: 4,
                }}>
                  {[0, 1, 2].map(i => (
                    <span key={i} style={{
                      width: 6, height: 6, borderRadius: 999,
                      background: TABI_COLORS.ink3,
                      animation: 'tabiBlink 1.2s ease-in-out infinite',
                      animationDelay: `${i * 0.18}s`,
                    }} />
                  ))}
                </div>
              )}
              {draft && draftPayload && (
                <div ref={draftRef} style={{
                  background: TABI_COLORS.card, borderRadius: 18, border: `1px solid ${TABI_COLORS.line}`,
                  padding: 14, boxShadow: TABI_COLORS.shadowSoft,
                }}>
                  {!draftExpanded ? (
                    <>
                      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
                        <div style={{
                          width: 38, height: 38, borderRadius: 14, display: 'grid', placeItems: 'center',
                          background: draftPayload.visibility === 'shared' ? `${TABI_COLORS.warn}22` : `${TABI_COLORS.red}18`,
                          color: draftPayload.visibility === 'shared' ? TABI_COLORS.warn : TABI_COLORS.red,
                          fontSize: 20,
                        }}>{draftPayload.visibility === 'shared' ? '割' : '私'}</div>
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <div style={{ fontSize: 11, fontWeight: 900, color: TABI_COLORS.red, letterSpacing: '0.08em', marginBottom: 4 }}>
                            {savedExpenseId ? '已新增支出' : '支出草稿'}
                          </div>
                          <div style={{ fontSize: 17, fontWeight: 900, color: TABI_COLORS.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                            {draftPayload.title || '未命名支出'}
                          </div>
                          <div style={{ marginTop: 5, fontSize: 13, color: TABI_COLORS.ink3, fontWeight: 700 }}>
                            {draftPayload.currency || 'JPY'} {Number(draftPayload.amount || 0).toLocaleString()} · {draftPayload.category || '其他'} · {draftPayload.visibility === 'shared' ? '共同支出' : '個人支出'}
                          </div>
                          {draftPayload.visibility === 'shared' && (
                            <div style={{ marginTop: 4, fontSize: 12, color: TABI_COLORS.ink3 }}>
                              {draftPayload.split_method === 'custom' ? '自訂金額' : '均分'}
                              {draftPayload.paid_by && members.find(m => m.id === draftPayload.paid_by) ? ` · ${members.find(m => m.id === draftPayload.paid_by).display_name} 付款` : ''}
                            </div>
                          )}
                        </div>
                      </div>
                      {warning && <div style={{ marginTop: 10, fontSize: 12, color: TABI_COLORS.red, lineHeight: 1.5 }}>{warning}</div>}
                      <div style={{ marginTop: 12, display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: 8 }}>
                        <button disabled={!savedExpenseId && (busy || draftPayload.amount <= 0 || !!warning)} onClick={() => savedExpenseId ? setDraftExpanded(true) : confirmDraft()} style={{
                          minHeight: 40, borderRadius: 13, border: 'none',
                          background: (!savedExpenseId && (busy || draftPayload.amount <= 0 || !!warning)) ? TABI_COLORS.line2 : TABI_COLORS.red,
                          color: '#fff', fontWeight: 900, cursor: (!savedExpenseId && (busy || draftPayload.amount <= 0 || !!warning)) ? 'not-allowed' : 'pointer',
                        }}>{savedExpenseId ? '查看' : '確認'}</button>
                        <button onClick={() => setDraftExpanded(true)} style={{
                          minHeight: 40, borderRadius: 13, border: `1px solid ${TABI_COLORS.line}`,
                          background: TABI_COLORS.bg, color: TABI_COLORS.ink2, fontWeight: 900, cursor: 'pointer',
                        }}>編輯</button>
                        <button onClick={() => { setDraft(null); setDraftPayload(null); setDraftExpanded(false); }} style={{
                          minHeight: 40, width: 46, borderRadius: 13, border: `1px solid ${TABI_COLORS.line}`,
                          background: TABI_COLORS.card, color: TABI_COLORS.ink3, fontWeight: 900, cursor: 'pointer',
                        }}>×</button>
                      </div>
                    </>
                  ) : (
                    <>
                  <div style={{ fontSize: 11, fontWeight: 900, color: TABI_COLORS.red, letterSpacing: '0.08em', marginBottom: 8 }}>
                    {savedExpenseId ? '編輯已新增支出' : '支出草稿'}
                  </div>
                  <div style={{ display: 'grid', gridTemplateColumns: '1fr 0.72fr', gap: 10 }}>
                    <label>
                      <span style={labelStyle}>名稱</span>
                      <input value={draftPayload.title || ''} onChange={e => patchPayload({ title: e.target.value })} style={fieldStyle} />
                    </label>
                    <label>
                      <span style={labelStyle}>金額</span>
                      <input inputMode="numeric" value={draftPayload.amount || ''} onChange={e => patchPayload({ amount: parseInt(e.target.value.replace(/,/g, ''), 10) || 0 })} style={fieldStyle} />
                    </label>
                  </div>
                  <div style={{ marginTop: 10, display: 'grid', gridTemplateColumns: '0.7fr 1fr 1fr', gap: 8 }}>
                    <label>
                      <span style={labelStyle}>幣別</span>
                      <select value={draftPayload.currency || 'JPY'} onChange={e => patchPayload({ currency: e.target.value })} style={fieldStyle}>
                        {['JPY', 'TWD', 'USD', 'KRW', 'HKD', 'EUR'].map(c => <option key={c} value={c}>{c}</option>)}
                      </select>
                    </label>
                    <label>
                      <span style={labelStyle}>分類</span>
                      <select value={draftPayload.category || '其他'} onChange={e => patchPayload({ category: e.target.value })} style={fieldStyle}>
                        {['餐飲', '交通', '購物', '住宿', '門票', '其他'].map(c => <option key={c} value={c}>{c}</option>)}
                      </select>
                    </label>
                    <label>
                      <span style={labelStyle}>日期</span>
                      <input type="date" value={dateValue} onChange={e => patchPayload({ expense_date: e.target.value ? `${e.target.value}T12:00:00+00:00` : null })} style={fieldStyle} />
                    </label>
                  </div>

                  <div style={{ marginTop: 12 }}>
                    <span style={labelStyle}>類型</span>
                    <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
                      {[
                        ['personal', '個人支出'],
                        ['shared', '共同支出'],
                      ].map(([v, label]) => {
                        const on = draftPayload.visibility === v;
                        return (
                          <button key={v} onClick={() => setVisibility(v)} style={{
                            minHeight: 39, borderRadius: 12, border: `1px solid ${on ? TABI_COLORS.red : TABI_COLORS.line}`,
                            background: on ? `${TABI_COLORS.red}12` : TABI_COLORS.bg,
                            color: on ? TABI_COLORS.red : TABI_COLORS.ink2,
                            fontWeight: 900, cursor: 'pointer',
                          }}>{label}</button>
                        );
                      })}
                    </div>
                  </div>

                  {draftPayload.visibility === 'shared' && (
                    <div style={{ marginTop: 10 }}>
                      <label>
                        <span style={labelStyle}>誰付錢</span>
                        <select value={draftPayload.paid_by || ''} onChange={e => patchPayload({ paid_by: e.target.value })} style={fieldStyle}>
                          {members.map(m => <option key={m.id} value={m.id}>{m.display_name}</option>)}
                        </select>
                      </label>
                    </div>
                  )}

                  {draftPayload.visibility === 'personal' && (
                    <div style={{ marginTop: 12, padding: 10, borderRadius: 14, background: TABI_COLORS.bg, border: `1px solid ${TABI_COLORS.line}` }}>
                      <label style={{ display: 'flex', alignItems: 'center', gap: 8, color: TABI_COLORS.ink2, fontSize: 13, fontWeight: 800 }}>
                        <input type="checkbox" checked={!!draftPayload.is_proxy} onChange={e => patchPayload({ is_proxy: e.target.checked, proxy_for: e.target.checked ? draftPayload.proxy_for : null })} />
                        這筆是代購
                      </label>
                      {draftPayload.is_proxy && (
                        <label style={{ display: 'block', marginTop: 8 }}>
                          <span style={labelStyle}>代購對象</span>
                          <input value={draftPayload.proxy_for || ''} onChange={e => patchPayload({ proxy_for: e.target.value })} placeholder="例如：Lucas" style={fieldStyle} />
                        </label>
                      )}
                    </div>
                  )}

                  {draftPayload.visibility === 'shared' && (
                    <div style={{ marginTop: 12, padding: 10, borderRadius: 14, background: TABI_COLORS.bg, border: `1px solid ${TABI_COLORS.line}` }}>
                      <span style={labelStyle}>共同支出分帳方式</span>
                      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
                        {[
                          ['equal', '均分'],
                          ['custom', '自訂金額'],
                        ].map(([v, label]) => {
                          const on = draftPayload.split_method === v;
                          return (
                            <button key={v} onClick={() => setSplitMethod(v)} style={{
                              minHeight: 38, borderRadius: 12, border: `1px solid ${on ? TABI_COLORS.red : TABI_COLORS.line}`,
                              background: on ? `${TABI_COLORS.red}12` : TABI_COLORS.card,
                              color: on ? TABI_COLORS.red : TABI_COLORS.ink2,
                              fontWeight: 900, cursor: 'pointer',
                            }}>{label}</button>
                          );
                        })}
                      </div>
                      {draftPayload.split_method === 'custom' && (
                        <div style={{
                          marginTop: 10,
                          display: 'grid',
                          gridTemplateColumns: 'repeat(auto-fit, minmax(132px, 1fr))',
                          gap: 8,
                        }}>
                          {members.map(m => (
                            <label key={m.id} style={{ display: 'block', minWidth: 0 }}>
                              <span style={{
                                display: 'block', color: TABI_COLORS.ink2, fontSize: 12.5, fontWeight: 800,
                                whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', marginBottom: 5,
                              }}>{m.display_name}</span>
                              <input inputMode="numeric" value={(draftPayload.splits || {})[m.id] || ''} onChange={e => setSplitAmount(m.id, e.target.value)} style={fieldStyle} />
                            </label>
                          ))}
                          <div style={{
                            gridColumn: '1 / -1',
                            fontSize: 11.5, color: splitTotal === Number(draftPayload.amount || 0) ? TABI_COLORS.green : TABI_COLORS.red, fontWeight: 800,
                          }}>
                            加總 {splitTotal.toLocaleString()} / 總額 {Number(draftPayload.amount || 0).toLocaleString()}
                          </div>
                        </div>
                      )}
                    </div>
                  )}
                  {warning && <div style={{ marginTop: 10, fontSize: 12, color: TABI_COLORS.red, lineHeight: 1.5 }}>{warning}</div>}
                  <div style={{ marginTop: 14, display: 'flex', gap: 8 }}>
                    <button disabled={busy || draftPayload.amount <= 0 || !!warning} onClick={confirmDraft} style={{
                      flex: 1, minHeight: 42, borderRadius: 14, border: 'none',
                      background: (busy || draftPayload.amount <= 0 || !!warning) ? TABI_COLORS.line2 : TABI_COLORS.red,
                      color: '#fff', fontWeight: 800, cursor: (busy || draftPayload.amount <= 0 || !!warning) ? 'not-allowed' : 'pointer',
                    }}>{savedExpenseId ? '更新支出' : '確認新增'}</button>
                    <button onClick={() => { setDraft(null); setDraftPayload(null); setDraftExpanded(false); }} style={{
                      minHeight: 42, padding: '0 14px', borderRadius: 14, border: `1px solid ${TABI_COLORS.line}`,
                      background: TABI_COLORS.card, color: TABI_COLORS.ink2, fontWeight: 800, cursor: 'pointer',
                    }}>取消</button>
                  </div>
                    </>
                  )}
                </div>
              )}
            </div>

            {!draftExpanded && (
              <div style={{ padding: '10px 12px calc(env(safe-area-inset-bottom, 0px) + 12px)', borderTop: `1px solid ${TABI_COLORS.line}`, background: 'rgba(255,250,242,0.94)' }}>
                <input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={e => { handleImagePick(e.target.files?.[0]); e.target.value = ''; }} />
                <div style={{ display: 'flex', gap: 7, alignItems: 'flex-end' }}>
                  <button onClick={() => fileRef.current?.click()} aria-label="上傳圖片" style={{
                    width: 48, height: 48, borderRadius: 16, border: `1px solid ${TABI_COLORS.line}`,
                    background: TABI_COLORS.card, color: TABI_COLORS.ink, fontSize: 24, fontWeight: 600, cursor: 'pointer',
                    lineHeight: 1,
                  }}>+</button>
                  <textarea value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => {
                    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
                  }} placeholder="輸入訊息…" style={{
                    flex: 1, resize: 'none', height: 48, minHeight: 48, maxHeight: 96, borderRadius: 16,
                    border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.card,
                    padding: '14px 12px 10px', fontSize: 16, lineHeight: 1.35, color: TABI_COLORS.ink,
                    fontFamily: tabiFont,
                  }} />
                  <button onClick={startVoiceInput} aria-label="語音輸入" style={{
                    width: 48, height: 48, borderRadius: 16, border: `1px solid ${listening ? TABI_COLORS.red : TABI_COLORS.line}`,
                    background: listening ? `${TABI_COLORS.red}14` : TABI_COLORS.card,
                    color: listening ? TABI_COLORS.red : TABI_COLORS.ink, cursor: 'pointer',
                    display: 'grid', placeItems: 'center',
                  }}>
                    <svg width="19" height="19" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                      <path d="M12 14.5a3.5 3.5 0 0 0 3.5-3.5V6a3.5 3.5 0 0 0-7 0v5a3.5 3.5 0 0 0 3.5 3.5Z" stroke="currentColor" strokeWidth="2" />
                      <path d="M5 10.5a7 7 0 0 0 14 0M12 17.5V21M8.5 21h7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
                    </svg>
                  </button>
                  <button disabled={busy || !input.trim()} onClick={sendMessage} style={{
                    width: 48, height: 48, borderRadius: 16, border: 'none',
                    background: busy || !input.trim() ? TABI_COLORS.line2 : TABI_COLORS.ink,
                    color: '#fff', fontWeight: 900, cursor: busy || !input.trim() ? 'not-allowed' : 'pointer',
                    display: 'grid', placeItems: 'center',
                  }}>{busy ? '…' : (
                    <svg width="19" height="19" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                      <path d="M21 3 10.6 13.4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
                      <path d="M21 3 14.4 21 10.6 13.4 3 9.6 21 3Z" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" />
                    </svg>
                  )}</button>
                </div>
              </div>
            )}
            {showHistory && (
              <div style={{
                position: 'absolute', inset: 0, zIndex: 4,
                background: 'rgba(24,20,15,0.24)',
                display: 'flex', justifyContent: 'flex-end',
              }} onClick={e => e.target === e.currentTarget && setShowHistory(false)}>
                <aside style={{
                  width: 'min(82vw, 360px)', height: '100%',
                  background: 'rgba(255,250,242,0.98)',
                  borderLeft: `1px solid ${TABI_COLORS.line}`,
                  boxShadow: '-18px 0 44px rgba(24,20,15,0.16)',
                  display: 'flex', flexDirection: 'column',
                  animation: 'tabiRise 0.16s ease-out',
                }}>
                  <div style={{
                    padding: 'calc(env(safe-area-inset-top, 0px) + 18px) 16px 12px',
                    borderBottom: `1px solid ${TABI_COLORS.line}`,
                    display: 'flex', alignItems: 'center', gap: 10,
                  }}>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontFamily: tabiSerif, fontSize: 20, fontWeight: 850, color: TABI_COLORS.ink }}>歷史對話</div>
                      <div style={{ marginTop: 3, fontSize: 11.5, color: TABI_COLORS.ink3 }}>切換後會載入完整紀錄</div>
                    </div>
                    <button onClick={() => setShowHistory(false)} style={{
                      width: 36, height: 36, borderRadius: 13, border: `1px solid ${TABI_COLORS.line}`,
                      background: TABI_COLORS.bg, color: TABI_COLORS.ink, fontSize: 20, cursor: 'pointer',
                    }}>×</button>
                  </div>
                  <div style={{ padding: 12, overflowY: 'auto', display: 'grid', gap: 8 }}>
                    <button onClick={() => createConversation(tripId || currentTripId || null)} style={{
                      width: '100%', minHeight: 42, borderRadius: 14, border: 'none',
                      background: TABI_COLORS.ink, color: '#fff', fontWeight: 950, cursor: 'pointer',
                    }}>＋ 新對話</button>
                    {conversations.length === 0 && (
                      <div style={{ fontSize: 12, color: TABI_COLORS.ink3, padding: '12px 2px' }}>還沒有歷史對話</div>
                    )}
                    {conversations.map(c => {
                      const active = c.id === conversationId;
                      return (
                        <div key={c.id} style={{
                          display: 'flex', alignItems: 'center', gap: 8,
                          border: `1px solid ${active ? TABI_COLORS.red : TABI_COLORS.line}`,
                          background: active ? `${TABI_COLORS.red}10` : TABI_COLORS.card,
                          borderRadius: 15,
                          padding: '12px 13px',
                          boxShadow: active ? '0 8px 18px rgba(185,74,53,0.08)' : 'none',
                        }}>
                          <button onClick={() => { setShowHistory(false); loadConversation(c.id); }} style={{
                            flex: 1, minWidth: 0, textAlign: 'left',
                            border: 'none', background: 'transparent', padding: 0,
                            color: TABI_COLORS.ink, cursor: 'pointer',
                          }}>
                            <div style={{ fontSize: 13.5, fontWeight: 950, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.title || '新的旅程對話'}</div>
                            <div style={{ marginTop: 5, fontSize: 11.5, color: TABI_COLORS.ink3, fontWeight: 750 }}>{c.updated_at ? new Date(c.updated_at).toLocaleString('zh-TW', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''}</div>
                          </button>
                          <button
                            onClick={(e) => {
                              e.stopPropagation();
                              if (window.confirm(`確定要刪除「${c.title || '新的旅程對話'}」這則對話嗎？此動作無法復原。`)) {
                                deleteConversation(c.id);
                              }
                            }}
                            title="刪除對話"
                            style={{
                              flex: '0 0 auto', width: 34, height: 34, borderRadius: 11,
                              border: `1px solid ${TABI_COLORS.line}`, background: TABI_COLORS.bg,
                              color: TABI_COLORS.redDeep, fontSize: 15, cursor: 'pointer',
                              display: 'grid', placeItems: 'center',
                            }}
                          >🗑</button>
                        </div>
                      );
                    })}
                  </div>
                </aside>
              </div>
            )}
          </div>
        </div>,
        document.body
      )}
    </>
  );
}
