/*__IIFE_WRAPPED__*/ ;(function(){ // screens-sub.jsx — Amulet Map / Wish List const { useState: useSub, useEffect: useESub } = React; const { useApp: useAppS, Icon: IconS, Btn: BtnS, Chip: ChipS, Card: CardS, Avatar: AvatarS, ImgSlot: ImgSlotS, Eyebrow: EyebrowS, TopBar: TopBarS, Body: BodyS, Field: FieldS, BottomNav: BottomNavS, GoogleMapView: GMapS, NAV_H, SAFE_TOP, Amulet, } = window; const ME_FALLBACK = 'あなた'; const PARTNER_FALLBACK = 'パートナー'; const SHIBUYA_CENTER = { lat: 35.6635, lng: 139.6960 }; /* ── Mini spot confirmation card — defined outside AddSpotModal to avoid remount ── */ function MiniSpotCard({ spot, confirmed, onConfirm, onRetry }) { return (
{spot.cat}
{spot.lat.toFixed(4)}, {spot.lng.toFixed(4)}
{spot.name}
{spot.note}
{confirmed?.name === spot.name && (
)}
); } /* ── Add Spot modal — chat-first. 画像→detectSpot / テキスト→手動候補 ── */ function AddSpotModal({ onClose, live, toast }) { const { useState: useS, useRef: useR, useEffect: useE } = React; const isLive = !!live?.coupleId; const [msgs, setMsgs] = useS([ { who: 'ai', text: '何を追加しますか?\n場所の写真を送るか、テキストで教えていただければ一緒に探します 📍' } ]); const [input, setInput] = useS(''); const [confirmed, setConfirmed] = useS(null); const [imageLoading, setImageLoading] = useS(false); const [done, setDone] = useS(false); const [lastImage, setLastImage] = useS(null); // { base64, mimeType } — 直近の画像。テキストヒントで再推論するため保持 const listRef = useR(null); const fileRef = useR(null); useE(() => { if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight; }, [msgs, imageLoading]); const addMsg = (msg) => setMsgs(m => [...m, msg]); // 会話履歴を detectSpot の chatContext({ role, text }[])へ変換。 // バックエンドが直近20件・4000字に丸めるので全件渡してよい。 const toChatContext = (list) => list .map(m => ({ role: m.who === 'ai' ? 'assistant' : 'user', text: (m.text || '').trim() })) .filter(m => m.text); // 画像 or テキストで場所推定を実行し、結果をチャットに反映(handleFile / sendChat 共通)。 // base64=null かつ textQuery を渡すとテキストのみ検索(Places API)になる。 const runDetect = async (base64, mimeType, chatContext, textQuery) => { setImageLoading(true); try { const r = await Amulet.detectSpot(base64, mimeType, chatContext, textQuery); setImageLoading(false); if (r.detected && r.spot) { const spot = { name: r.spot.name, lat: r.spot.lat, lng: r.spot.lng, note: r.spot.note, cat: r.spot.category, photoUrl: r.spot.photoUrl, placeId: r.spot.placeId, address: r.spot.address, rating: r.spot.rating, mapsUri: r.spot.mapsUri }; setConfirmed(spot); addMsg({ who: 'ai', text: '見つかりました!追加したいのはこの場所ですか?', spot }); } else { addMsg({ who: 'ai', text: base64 ? 'この写真からは場所が分かりませんでした…。お店の名前や地域をテキストで教えてもらえますか?' : '場所が見つかりませんでした…。別の名前や地域で試してみてもらえますか?' }); } } catch (err) { console.error(err); setImageLoading(false); addMsg({ who: 'ai', text: 'ごめんなさい、解析に失敗してしまいました。もう一度試してみてください。' }); } }; const handleImageAttach = () => { if (imageLoading || confirmed) return; if (fileRef.current) fileRef.current.click(); // 画像 → detectSpot(実API)で場所特定 }; const handleFile = async (e) => { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; const ctx = toChatContext(msgs); // 添付前までの会話をヒントに try { const { base64, mimeType } = await Amulet.fileToBase64(file); setLastImage({ base64, mimeType }); // テキストヒントで再推論できるよう保持 // アップロードした画像そのものをチャット履歴に表示(テキスト表記より内容が分かりやすい)。 addMsg({ who: 'user', image: `data:${mimeType};base64,${base64}` }); await runDetect(base64, mimeType, ctx); } catch (err) { console.error(err); addMsg({ who: 'ai', text: 'ごめんなさい、画像の解析に失敗してしまいました。テキストでも教えてもらえますか?' }); } }; const sendChat = () => { if (!input.trim() || confirmed || imageLoading) return; const text = input; setInput(''); const nextMsgs = [...msgs, { who: 'user', text }]; // 送ったヒントを確実に文脈へ含める addMsg({ who: 'user', text }); if (lastImage) { // 保持画像 + ヒントを含む会話で再推論(テキスト情報を場所検索の入力として活用)。 runDetect(lastImage.base64, lastImage.mimeType, toChatContext(nextMsgs)); return; } // 画像なし: テキストのみで Places API 検索。 runDetect(null, null, toChatContext(nextMsgs), text); }; const handleConfirm = async () => { const spot = confirmed; setConfirmed(null); if (isLive) { try { await Amulet.addPin(live.coupleId, { name: spot.name, category: spot.cat, lat: spot.lat, lng: spot.lng, note: spot.note, photoUrl: spot.photoUrl, placeId: spot.placeId, address: spot.address, rating: spot.rating, mapsUri: spot.mapsUri }); } catch (e) { console.error(e); toast && toast(Amulet.friendlyError(e)); addMsg({ who: 'ai', text: '追加に失敗してしまいました。もう一度試してみてください。' }); return; } } setDone(true); addMsg({ who: 'ai', text: `「${spot.name}」をAmulet Map に追加しました ✨` }); setTimeout(onClose, 1400); }; const handleRetry = () => { setConfirmed(null); addMsg({ who: 'ai', text: 'なるほど。もう少し詳しく教えてもらえますか?' }); }; return (
{/* backdrop */}
{/* sheet */}
{/* drag handle */}
{/* header */}
スポットを追加
{/* message list */}
{msgs.map((m, i) => (
{m.image ? ( 添付した写真 ) : (
{m.text}
)} {m.spot && }
))} {imageLoading && (
場所を確認中…
)}
{/* input bar */}