107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import Link from "next/link";
|
||
import { api } from "@/lib/api";
|
||
import { useI18n } from "@/lib/i18n";
|
||
import { useToast } from "@/lib/toast";
|
||
import RingNext from "@/components/RingNext";
|
||
import { useRingSteps } from "@/lib/rings";
|
||
|
||
const SUGGESTIONS = [
|
||
"预算 6000,想找网速好的东南亚城市",
|
||
"适合第一次远程办公的城市?",
|
||
"清迈和巴厘岛怎么选?",
|
||
];
|
||
|
||
export default function AiAssistantClient() {
|
||
const { t } = useI18n();
|
||
const { toast } = useToast();
|
||
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);
|
||
}
|
||
};
|
||
|
||
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>
|
||
<Link href="/plan" className="btn btn-sm">
|
||
写入计划
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<RingNext steps={rings.afterDiscover} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|