Skip to content

Commit fe814eb

Browse files
committed
スポット詳細に、Claude・Geminiへ質問を持って行くボタンを足す
スポット詳細のWikipediaアイコンの隣にClaude、Google マップの経路アイコンの 隣にGeminiのボタンを置いた。押すと質問文を入れた状態でそれぞれのページが 新しいタブで開く。質問文は lib/askAi.ts が組み、所在地と座標を添える ——「光明寺」のように同名の場所が各地にあり、名前だけだと別のスポットに ついて答えられてしまうため。 Geminiは gemini.google.com がURLでのプロンプト事前入力に対応していない (Chrome拡張で補う方法しか無い)ため、同じモデルが答える検索のAIモード (udm=50)を開いている。対応したら差し替えられるよう、URLの組み立ては lib/askAi.ts に寄せた。Claudeは公式の ?q=(プロンプト欄を埋めるだけで 送信はしない)。 アイコンはWikipedia・Google マップと同じくSimple Icons(CC0)。 Wikipediaボタンと違い種別設定での出し分けはしない —— 記事が無いスポットでも AIなら何か分かる可能性があるため。 あわせて、前回の呼称変更で見落としていたCLAUDE.mdの見出し2つを経路に直した。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 88e8dfa commit fe814eb

3 files changed

Lines changed: 96 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ PATCH `/api/spot-types/[id]`は`series_styles`・`categories`と同じく、保
193193

194194
カテゴリの一覧(種別ごとに使える値の定義)も同じパターンでスポット種別ごとに持つ(`lib/category.ts`)。`spot_type_settings`の`categories`キー(`CATEGORIES_SETTING_KEY`)にJSON文字列(`string[]`。見た目は持たない)を保存し、行が無い・parse失敗時は`DEFAULT_CATEGORIES`(観光地の現行カテゴリ、旧`lib/types.ts`の`CATEGORIES`ハードコードの後継)にフォールバックする(`resolveCategories`)。明示的に空配列`"[]"`を保存した種別は「定義済みカテゴリなし」の扱い。配列の並び順がカテゴリの並び順(`getCategoryOrder`)で、地図・スポット一覧の絞り込みチップ(`components/FilterBar.tsx`の`SpotFilters.categories`。シリーズと同じ複数選択+「すべて」チップ(訪問状況は「すべて」チップ無しで既定=未訪問のみ)、選択肢は実データに存在する値から作る)と、スポット追加・編集フォーム(`AddSpotModal`)の選択チップ(複数選択のトグル。設定の一覧を先頭に、設定外の既存値を後ろに合成し、一覧に無い値は下の入力欄から足せる)がこの並びを使う。取得は`useCategories(typeKey)`フック(`useSeriesStyles`のカテゴリ版)。管理画面`/[type]/admin`「スポット種別の設定」のカテゴリ欄(カンマ・読点区切りで入力、admin専用)と、スポット種別JSON作成の`categories`フィールド(文字列配列)から設定でき、PATCH `/api/spot-types/[id]`が`parseCategories`で妥当性を検証する。`categories`列自体は従来どおり自由入力で、一覧に無い値も動く(並びは末尾)。
195195

196-
### ルート(`spot_routes`/`spot_route_points`)とスポット参照キー(`spots.key`)
196+
### 経路(`spot_routes`/`spot_route_points`)とスポット参照キー(`spots.key`)
197197

198198
スポットを「巡った順」に矢印で繋ぐルート機能(訪問順に意味がある種別向け)。スキーマは`db/init/01_schema.sql`(他のテーブルと同じファイル)。`spot_routes`(種別ごとのルート名、`(spot_type_id, name)`一意。加えて色分け・絞り込み連動に使う`series`列、ルート詳細に表示するルート全体の説明文の`description`列、spotsと同じ公開状態の`status`/`created_by`列 — 公開=全員、非公開=作成者本人のみ、承認待ち・却下=本人+moderator以上がGET/DELETEの権限判定に使われる。ルートCSVインポートはスポットと同じく常に`status: 'published'`を明示する)+`spot_route_points`(route_id, seq, spot_id, description — 点側の`description`はその経由地から次の経由地への**区間の説明**(移動手段など)で、次の区間が無い最終地点は常にnull)の2テーブルで、経由地はスポット削除時にFKカスケードで点だけ抜け、ルートは残る。公開スポットの全削除(purge)はルートも丸ごと消し、種別削除は`spot_types`へのFKカスケードで消える。
199199

@@ -219,7 +219,16 @@ PATCH `/api/spot-types/[id]`は`series_styles`・`categories`と同じく、保
219219

220220
同じ仕組みで、絞り込みモーダルの訪問日の下に**「訪問予定リスト」セレクト**があり、選んだリスト(旅程)のスポットを**リスト順に矢印(紫`PLAN_LIST_PATH_COLOR`=`#9333ea`)で結んだ経路**を描く(`filters.planListId`・`buildPlanListPath`。**訪問済みの経由スポットは経路に載せない** —— 済んだ場所を通り続ける線が残ると次にどこへ行くかが読めないため。リスト自体からは消えない)。訪問日の経路と同じ`spot-routes`ソース/レイヤーに色違いで重ねるだけで(`buildRouteGeoJSON`は色付き経路の配列`extraPaths`を受け取る)、選んだときに`handleSelectPlanList`が経路全体へ`fitBounds`する点も訪問日と同じ(**経路上でもピンは絞り込みに従う**。上記「ピンの表示と線の表示を分ける」)。**現在地(GeolocateControlの青丸)を表示中は、現在地からリスト先頭のスポットまでも線・矢印で結ぶ。この区間だけは青丸と同じ青(`CURRENT_LOCATION_PATH_COLOR`=maplibre-gl既定の`#1da1f2`)で描き、現在地から出ている線だと分かるようにする**(`buildRouteGeoJSON`の`start`に`currentLocation` stateを、`startColor`にこの青を渡すと、この区間が独立したLineStringとして経路の先頭に繋がる。現在地は`geolocate`イベントで更新し(約1m未満の移動は再レンダー抑制のため無視)、青丸ごと消えるOFFへの遷移で忘れる — `trackuserlocationend`はBACKGROUND=青丸が残る遷移でも発火するため、青丸のDOM要素の有無でOFFを見分ける)。リストは`MapView`が`api.visitPlanLists.list`で読み(1件以上あるときだけセレクトを出す)、選択は`loadSavedFilters`/`saveFilters`で保存する(削除済み等で見つからないIDは描画側で無視)。訪問予定リストのセクションのリセットボタンが`planListId`を`null`に戻す(見出し行のリセットは絞り込みのみで触らない)。
221221

222-
### ルート・経路をGoogle マップで開く
222+
### 経路・訪問順・訪問予定リストをGoogle マップで開く
223+
224+
### スポットについて生成AIに聞く(`lib/askAi.ts`)
225+
226+
スポット詳細に**Claude**(Wikipediaアイコンの隣)と**Gemini**(Google マップの経路アイコンの隣)のボタンを置いてある。押すと**質問文を入れた状態**でそれぞれのページが新しいタブで開く。質問文は`buildSpotQuestion`が組み、**所在地と座標を添える** —— 「光明寺」のように同名の場所が各地にあるため、名前だけだと別のスポットについて答えられてしまう。
227+
228+
- **Claudeは`https://claude.ai/new?q=<質問文>`**`q`はプロンプト欄を埋めるだけで送信はしない公式のパラメータ(送る前に読み返せる)
229+
- **Geminiは`https://www.google.com/search?udm=50&q=<質問文>`(検索のAIモード)****`gemini.google.com`はURLでプロンプトを渡せない**(Chrome拡張で補う方法しか無い)ため、同じモデルが答えるAIモードを開いている。gemini.google.com側が対応したら差し替えられるよう、URLの組み立ては`lib/askAi.ts`に寄せてある
230+
- アイコンはWikipedia・Google マップと同じくSimple Icons(CC0)のロゴマークをインラインSVGで持つ
231+
- **Wikipediaボタンと違い、種別の設定で消さない**(`wikipedia_enabled`のような出し分けをしない) —— Wikipedia記事が無いスポットでも何か分かる可能性があるため
223232

224233
### 用語: 「経路」と、記録から引かれる線
225234

components/SpotDetailModal.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
type VisitPlanList,
2121
} from "@/lib/types";
2222
import { formatPlanDateRange } from "@/lib/planListDraft";
23+
import { buildClaudeAskUrl, buildGeminiAskUrl } from "@/lib/askAi";
2324
import SeriesBadge from "@/components/SeriesBadge";
2425
import MiniMap from "@/components/MiniMap";
2526
import { resolveSeriesStyles } from "@/lib/seriesStyle";
@@ -49,6 +50,24 @@ export function WikipediaIcon({ className }: { className?: string }) {
4950
);
5051
}
5152

53+
/** Claudeの公式ロゴマーク(Simple Icons、CC0) */
54+
function ClaudeIcon({ className }: { className?: string }) {
55+
return (
56+
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
57+
<path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z" />
58+
</svg>
59+
);
60+
}
61+
62+
/** Google Geminiの公式ロゴマーク(Simple Icons、CC0) */
63+
function GeminiIcon({ className }: { className?: string }) {
64+
return (
65+
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
66+
<path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81" />
67+
</svg>
68+
);
69+
}
70+
5271
/** Google マップの公式ロゴマーク(Simple Icons、CC0) */
5372
function GoogleMapsIcon({ className }: { className?: string }) {
5473
return (
@@ -575,6 +594,19 @@ export default function SpotDetailModal({
575594
<WikipediaIcon className="size-5" />
576595
</button>
577596
)}
597+
{/* スポットについてAIに聞く。質問文(所在地・座標つき)を入れた状態で
598+
新しい会話を開く。Wikipedia記事が無いスポットでも何か分かるように、
599+
Wikipediaの有無に関わらず出す */}
600+
<a
601+
href={buildClaudeAskUrl(spot)}
602+
target="_blank"
603+
rel="noopener noreferrer"
604+
aria-label="Claudeにこのスポットについて聞く"
605+
title="Claudeにこのスポットについて聞く"
606+
className="rounded p-1 text-blue-600 hover:bg-blue-50"
607+
>
608+
<ClaudeIcon className="size-5" />
609+
</a>
578610
<div className="ml-auto flex items-center gap-1 text-sm text-gray-500">
579611
Google:
580612
<a
@@ -605,6 +637,18 @@ export default function SpotDetailModal({
605637
>
606638
<DirectionsIcon className="size-5" />
607639
</a>
640+
{/* Geminiに聞く。gemini.google.com はURLでプロンプトを渡せないため、
641+
同じモデルが答える検索のAIモード(udm=50)を開く(lib/askAi.ts) */}
642+
<a
643+
href={buildGeminiAskUrl(spot)}
644+
target="_blank"
645+
rel="noopener noreferrer"
646+
aria-label="Geminiにこのスポットについて聞く"
647+
title="Geminiにこのスポットについて聞く"
648+
className="rounded p-1 text-blue-600 hover:bg-blue-50"
649+
>
650+
<GeminiIcon className="size-5" />
651+
</a>
608652
</div>
609653
</div>
610654

lib/askAi.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Spot } from "@/lib/types";
2+
3+
/**
4+
* スポットについて生成AIに聞くための質問文と、その質問を入れた状態でAIのページを
5+
* 開くURLを組み立てる。スポット詳細のClaude・Geminiのボタンで使う。
6+
*
7+
* **同名の別スポットに答えられてしまうのを防ぐため、質問文に所在地と座標を添える**
8+
* (「光明寺」のように同じ名前の場所が各地にあるため)。
9+
*/
10+
11+
/** 質問文。AIのページのテキストボックスにこの文字列が入る */
12+
export function buildSpotQuestion(spot: Spot): string {
13+
const where = [spot.region, `${spot.lat.toFixed(5)},${spot.lng.toFixed(5)}`]
14+
.filter(Boolean)
15+
.join(" / ");
16+
return (
17+
`「${spot.name}」(${where})について教えてください。` +
18+
`どんな場所で何が見どころか、歴史や由来、` +
19+
`訪れるときに知っておくとよいこと(見学の所要時間・混む時期・近くの立ち寄り先)を知りたいです。`
20+
);
21+
}
22+
23+
/**
24+
* Claudeを新しい会話で開くURL。`q`はプロンプト欄に入った状態で開く公式の
25+
* パラメータ(送信はされないので、送る前に読み返せる)。
26+
*/
27+
export function buildClaudeAskUrl(spot: Spot): string {
28+
return `https://claude.ai/new?q=${encodeURIComponent(buildSpotQuestion(spot))}`;
29+
}
30+
31+
/**
32+
* Gemini(Google検索のAIモード)で開くURL。
33+
* **gemini.google.com はURLでプロンプトを渡せない**ため、同じGeminiのモデルが
34+
* 答えるAIモード(`udm=50`)を使う。検索ボックスに質問文が入った状態で回答が出る。
35+
* gemini.google.com側がURLでの事前入力に対応したら、そちらへ変えられる。
36+
*/
37+
export function buildGeminiAskUrl(spot: Spot): string {
38+
return `https://www.google.com/search?udm=50&q=${encodeURIComponent(
39+
buildSpotQuestion(spot)
40+
)}`;
41+
}

0 commit comments

Comments
 (0)