nomadweb/frontend/src/components/AiAssistantClient.tsx
2026-08-30 21:37:57 -05:00

135 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { api } from "@/lib/api";
import { useI18n } from "@/lib/i18n";
import { useToast } from "@/lib/toast";
import { mergeDestinationsIntoTrip } from "@/lib/tripActions";
import type { Destination } from "@/lib/types";
import RingNext from "@/components/RingNext";
import { useRingSteps } from "@/lib/rings";
const SUGGESTIONS = [
"预算 6000,想找网速好的东南亚城市",
"适合第一次远程办公的城市?",
"清迈和巴厘岛怎么选?",
];
interface Props {
destinations: Destination[];
}
export default function AiAssistantClient({ destinations }: Props) {
const { t } = useI18n();
const { toast } = useToast();
const router = useRouter();
const rings = useRingSteps();
const [message, setMessage] = useState("");
const [reply, setReply] = useState("");
const [cities, setCities] = useState<{ slug: string; name: string; emoji: string }[]>([]);
const [loading, setLoading] = useState(false);
const ask = async (text?: string) => {
const q = (text ?? message).trim();
if (!q) {
toast("先写一个问题", "info");
return;
}
setMessage(q);
setLoading(true);
try {
const res = await api.askAssistant(q);
setReply(res.reply);
setCities(res.cities || []);
} catch {
setReply(t.ai.fail);
setCities([]);
} finally {
setLoading(false);
}
};
const addToPlan = () => {
const dests = cities
.map((c) => destinations.find((d) => d.slug === c.slug))
.filter((d): d is Destination => Boolean(d));
if (dests.length === 0) {
toast("推荐城市暂无完整数据,请从详情页加入计划", "info");
router.push("/plan");
return;
}
const { added, skipped } = mergeDestinationsIntoTrip(dests, 1);
if (added === 0) {
toast(skipped ? t.compare.alreadyInPlan : "未能写入计划", "info");
} else {
toast(`${t.compare.addedN} ${added} ${t.compare.citiesUnit}`);
}
router.push("/plan");
};
return (
<div className="community-page">
<div className="container">
<nav className="detail-nav">
<Link href="/">{t.common.backHome}</Link>
<span> · </span>
<Link href="/next-stop">{t.ai.nextStop}</Link>
</nav>
<div className="section-header reveal">
<span className="section-tag">{t.ai.tag}</span>
<h1>{t.ai.title}</h1>
<p>{t.ai.subtitle}</p>
</div>
<div className="ai-suggestions reveal">
{SUGGESTIONS.map((s) => (
<button key={s} type="button" className="filter-btn" disabled={loading} onClick={() => void ask(s)}>
{s}
</button>
))}
</div>
<div className="community-compose reveal">
<textarea
rows={4}
placeholder={t.ai.placeholder}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void ask();
}}
/>
<button type="button" className="btn btn-primary" disabled={loading} onClick={() => void ask()}>
{loading ? "思考中…" : t.ai.ask}
</button>
<p className="section-desc">Ctrl/⌘ + Enter 发送</p>
</div>
{reply && (
<div className="ai-reply reveal">
<p>{reply}</p>
<div className="ai-cities">
{cities.map((c) => (
<Link key={c.slug} href={`/destinations/${c.slug}`} className="btn btn-sm">
{c.emoji} {c.name}
</Link>
))}
<Link href="/next-stop" className="btn btn-sm">
{t.ai.nextStop}
</Link>
<Link href="/compare" className="btn btn-sm">
城市对比
</Link>
{cities.length > 0 && (
<button type="button" className="btn btn-sm btn-primary" onClick={addToPlan}>
写入计划
</button>
)}
</div>
</div>
)}
<RingNext steps={rings.afterDiscover} />
</div>
</div>
);
}