manual save(2026-01-22 15:55)

This commit is contained in:
SiteAgent Bot
2026-01-22 15:55:42 +08:00
parent 74f78d5f6d
commit 2fda84a220
2 changed files with 732 additions and 21 deletions

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react' import React from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { ChevronRight, Home as HomeIcon } from 'lucide-react' import { ChevronRight, Home as HomeIcon } from 'lucide-react'
import { getLocaleFromPathname, stripLocalePrefix, withLocalePath } from '../lib/i18n' import { getLocaleFromPathname, withLocalePath } from '../lib/i18n'
export type BreadcrumbItem = { export type BreadcrumbItem = {
label: string label: string
@@ -15,48 +15,40 @@ type BreadcrumbsProps = {
export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ items, className }) => { export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ items, className }) => {
const location = useLocation() const location = useLocation()
const locale = useMemo(() => getLocaleFromPathname(location.pathname), [location.pathname]) const locale = getLocaleFromPathname(location.pathname)
const homeLabel = locale === 'en' ? 'Home' : '首页'
const homeHref = withLocalePath(locale, '/')
const normalizedItems = useMemo(() => {
const prefix = stripLocalePrefix(location.pathname).startsWith('/') ? '' : '/'
void prefix
return items
}, [items, location.pathname])
return ( return (
<nav aria-label={locale === 'en' ? 'Breadcrumb' : '面包屑导航'} className={className}> <nav aria-label={locale === 'en' ? 'Breadcrumb' : '面包屑导航'} className={className}>
<ol className="flex flex-wrap items-center gap-1 text-sm text-gray-600"> <ol className="flex flex-wrap items-center gap-1 text-sm text-white/85">
<li className="flex items-center gap-1"> <li className="flex items-center gap-1">
<Link <Link
to={homeHref} to={withLocalePath(locale, '/')}
className="inline-flex items-center gap-1 rounded-md px-2 py-1 hover:bg-white/60 hover:text-primary transition-colors" className="inline-flex items-center gap-1 rounded-md px-2 py-1 hover:bg-white/10 hover:text-white transition-colors"
aria-label={locale === 'en' ? 'Go to homepage' : '返回首页'} aria-label={locale === 'en' ? 'Go to homepage' : '返回首页'}
> >
<HomeIcon size={16} /> <HomeIcon size={16} />
<span>{homeLabel}</span> <span>{locale === 'en' ? 'Home' : '首页'}</span>
</Link> </Link>
</li> </li>
{normalizedItems.map((item, index) => { {items.map((item, index) => {
const isLast = index === normalizedItems.length - 1 const isLast = index === items.length - 1
return ( return (
<React.Fragment key={`${item.label}-${index}`}> <React.Fragment key={`${item.label}-${index}`}>
<li aria-hidden="true" className="text-gray-400"> <li aria-hidden="true" className="text-white/50">
<ChevronRight size={16} /> <ChevronRight size={16} />
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
{item.href && !isLast ? ( {item.href && !isLast ? (
<Link <Link
to={withLocalePath(locale, item.href)} to={withLocalePath(locale, item.href)}
className="rounded-md px-2 py-1 hover:bg-white/60 hover:text-primary transition-colors" className="rounded-md px-2 py-1 hover:bg-white/10 hover:text-white transition-colors"
> >
{item.label} {item.label}
</Link> </Link>
) : ( ) : (
<span className="px-2 py-1 text-gray-700 font-medium" aria-current={isLast ? 'page' : undefined}> <span className="px-2 py-1 text-white font-medium" aria-current={isLast ? 'page' : undefined}>
{item.label} {item.label}
</span> </span>
)} )}

719
src/pages/Assistant.tsx Normal file
View File

@@ -0,0 +1,719 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { motion } from 'framer-motion'
import {
Bot,
CheckCircle,
ClipboardList,
Loader2,
MessageSquare,
Phone,
Send,
Sparkles,
User,
} from 'lucide-react'
import { Header } from '../components/Header'
import { Footer } from '../components/Footer'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { FormSubmissions, Forms, SearchResults } from '../clientsdk/sdk.gen'
import { createClient } from '../clientsdk/client'
import { customQuerySerializer } from '../clientsdk/querySerializer'
import { API_URL, TENANT_API_KEY, TENANT_SLUG } from '../config'
import { usePageMeta } from '../hooks/usePageMeta'
import { PAGE_META } from '../lib/constants'
import { getLocaleFromPathname } from '../lib/i18n'
type ChatRole = 'user' | 'assistant'
type ChatMessage = {
id: string
role: ChatRole
content: string
}
type LeadFormData = {
name: string
phone: string
company: string
demand: string
}
type LeadFormErrors = Partial<Record<keyof LeadFormData, string>>
type Locale = 'zh' | 'en'
const client = createClient({
baseUrl: API_URL,
querySerializer: customQuerySerializer,
headers: {
'X-Tenant-Slug': TENANT_SLUG,
'X-API-Key': TENANT_API_KEY,
},
})
function uuid() {
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`
}
function normalizeText(value: string) {
return value.trim().replace(/\s+/g, ' ')
}
function isValidPhone(phone: string) {
return /^1\d{10}$/.test(phone)
}
function stripHtml(html: string) {
const tmp = document.createElement('div')
tmp.innerHTML = html
return (tmp.textContent || tmp.innerText || '').trim()
}
function buildFallbackAnswer(locale: Locale, question: string) {
const q = question.replace(/\s+/g, '')
if (locale === 'en') {
return [
"I couldn't find an exact match in the knowledge base.",
'Please leave your phone number and consulting needs, and our team will follow up.',
].join('\n')
}
if (/北京.*高新.*(认定|企业)|高新企业认定|高新技术企业/.test(q)) {
return [
'关于“北京高新技术企业认定”的常见条件(示例整理,具体以最新政策与主管部门口径为准):',
'',
'1基本门槛',
'- 企业依法注册、正常经营,近年无重大违法违规记录。',
'- 主要产品/服务属于国家重点支持的高新技术领域。',
'',
'2核心评审维度常见',
'- 知识产权:发明/实用新型/软著等数量、质量与关联度。',
'- 科技人员占比:研发与技术人员结构、社保/劳动关系等合规性。',
'- 研发投入:近三年研发费用归集口径与占比。',
'- 高新收入占比:高新技术产品(服务)收入与主营结构。',
'- 创新能力:成果转化、组织管理、成长性等。',
'',
'3材料准备建议常见清单',
'- 企业基本资料:营业执照、章程、财务报表/审计等。',
'- 知识产权与研发项目:证书、研发立项/结题、费用归集凭证。',
'- 产品/服务说明:技术领域、核心技术说明、收入佐证材料。',
'',
'如果你方便补充:行业 + 主要产品/服务 + 已有知识产权情况,我可以按你企业现状给出更具体的材料清单与推进路径。',
'',
'若需要人工进一步对接,请在右侧“留资”填写手机号(必填),我们会尽快联系。',
].join('\n')
}
return [
'我暂时无法在知识库中定位到完全匹配的答案。',
'你可以补充更多背景(行业/地区/需求细节),或者直接在“留资”里提交手机号,我们安排人工跟进。',
].join('\n')
}
async function searchKnowledge(question: string) {
const response = await SearchResults.listSearchResults({
client,
query: {
limit: 5,
depth: 2,
where: {
title: {
contains: question,
},
} as any,
},
})
return ((response as any)?.data?.docs ?? []) as any[]
}
async function resolveLeadFormId() {
const res = await Forms.listForms({
client,
query: {
limit: 100,
depth: 0,
},
})
const forms = ((res as any)?.data?.docs ?? []) as any[]
const target = forms.find((f) => {
const title = String(f?.title || '')
return /AI\s*智能助手|诚裕智能客服|AI Assistant|Assistant Lead|留资/i.test(title)
})
return (target?.id as string | undefined) ?? null
}
export const Assistant: React.FC = () => {
const locale = useMemo(() => getLocaleFromPathname(window.location.pathname), [])
usePageMeta({
title: locale === 'en' ? 'AI Assistant' : PAGE_META.assistant.title,
description: PAGE_META.assistant.description,
locale,
})
const [activeTab, setActiveTab] = useState<'chat' | 'lead'>('chat')
const [messages, setMessages] = useState<ChatMessage[]>(() => [
{
id: uuid(),
role: 'assistant',
content:
locale === 'en'
? "Hi, I'm Chengyu AI Customer Service. Ask me anything, or leave your phone number for a callback."
: '你好,我是“诚裕智能客服”。你可以直接提问,我会基于知识库为你快速解答;如需人工进一步对接,也可以在“留资”提交手机号。',
},
])
const [input, setInput] = useState('')
const [isReplying, setIsReplying] = useState(false)
const [leadFormId, setLeadFormId] = useState<string | null>(null)
const [leadFormLoading, setLeadFormLoading] = useState(true)
const [lead, setLead] = useState<LeadFormData>({ name: '', phone: '', company: '', demand: '' })
const [leadErrors, setLeadErrors] = useState<LeadFormErrors>({})
const [leadSubmitting, setLeadSubmitting] = useState(false)
const [leadSubmitted, setLeadSubmitted] = useState(false)
const [leadSubmitError, setLeadSubmitError] = useState<string | null>(null)
const listRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const load = async () => {
try {
setLeadFormLoading(true)
const id = await resolveLeadFormId()
setLeadFormId(id)
} catch {
setLeadFormId(null)
} finally {
setLeadFormLoading(false)
}
}
load()
}, [])
useEffect(() => {
listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: 'smooth' })
}, [messages, isReplying])
const exampleQuestions = useMemo(
() =>
locale === 'en'
? [
'What are typical criteria for high-tech enterprise recognition in Beijing?',
'What materials are usually required?',
]
: ['北京高新企业认定需要哪些条件?', '高新企业申报通常需要准备哪些材料?'],
[locale],
)
const pushMessage = (role: ChatRole, content: string) => {
setMessages((prev) => [...prev, { id: uuid(), role, content }])
}
const replyTo = async (rawQuestion: string) => {
const question = normalizeText(rawQuestion)
if (!question) return
pushMessage('user', question)
setInput('')
setIsReplying(true)
try {
const docs = await searchKnowledge(question)
const top = docs[0]
if (docs.length > 0 && top?.doc?.value) {
const post = top.doc.value
const title = post?.title || top?.title
const excerpt = stripHtml(post?.content_html || '')
const answer =
locale === 'en'
? [
`Based on the knowledge base: "${title}"`,
'',
excerpt ? excerpt.slice(0, 420) + (excerpt.length > 420 ? '…' : '') : '',
'',
'If you want a human follow-up, please leave your phone number in the lead form.',
]
.filter(Boolean)
.join('\n')
: [
`已为你从知识库匹配到:${title}`,
'',
excerpt ? excerpt.slice(0, 320) + (excerpt.length > 320 ? '…' : '') : '(该条目暂无摘要内容)',
'',
'若需要人工进一步沟通,建议在“留资”提交手机号(必填),我们会尽快联系。',
].join('\n')
pushMessage('assistant', answer)
return
}
const fallback = buildFallbackAnswer(locale, question)
pushMessage('assistant', fallback)
setActiveTab('lead')
} catch {
pushMessage(
'assistant',
locale === 'en'
? 'Sorry, I cannot access the knowledge base right now. Please leave your phone number and we will contact you.'
: '抱歉,我暂时无法访问知识库。请在“留资”提交手机号,我们会尽快联系你。',
)
setActiveTab('lead')
} finally {
setIsReplying(false)
}
}
const handleSend = async () => {
if (isReplying) return
await replyTo(input)
}
const validateLead = () => {
const next: LeadFormErrors = {}
if (!lead.phone.trim()) next.phone = locale === 'en' ? 'Phone is required' : '手机号为必填项'
else if (!isValidPhone(lead.phone.trim())) next.phone = locale === 'en' ? 'Invalid 11-digit phone' : '请输入有效的 11 位手机号'
if (lead.demand.trim().length < 6) next.demand = locale === 'en' ? 'Please provide more details' : '请简单描述需求(至少 6 个字符)'
setLeadErrors(next)
return Object.keys(next).length === 0
}
const submitLead = async (e: React.FormEvent) => {
e.preventDefault()
setLeadSubmitError(null)
if (!validateLead()) return
setLeadSubmitting(true)
try {
if (!leadFormId) {
const cacheKey = 'chengyu_leads_local'
const record = {
...lead,
createdAt: new Date().toISOString(),
source: 'assistant',
}
const existing = JSON.parse(localStorage.getItem(cacheKey) || '[]')
localStorage.setItem(cacheKey, JSON.stringify([record, ...existing]))
setLeadSubmitted(true)
setLead({ name: '', phone: '', company: '', demand: '' })
setTimeout(() => setLeadSubmitted(false), 3500)
return
}
await FormSubmissions.createFormSubmission({
client,
body: {
form: leadFormId,
submissionData: [
{ field: 'name', value: lead.name.trim() },
{ field: 'phone', value: lead.phone.trim() },
{ field: 'company', value: lead.company.trim() },
{ field: 'demand', value: lead.demand.trim() },
{ field: 'source', value: 'AI智能助手' },
].filter((item) => item.value),
},
} as any)
setLeadSubmitted(true)
setLead({ name: '', phone: '', company: '', demand: '' })
setTimeout(() => setLeadSubmitted(false), 3500)
} catch {
setLeadSubmitError(locale === 'en' ? 'Submission failed. Please try again.' : '提交失败,请稍后重试')
} finally {
setLeadSubmitting(false)
}
}
return (
<motion.div className="min-h-screen bg-background" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.4 }}>
<Header />
<main>
<section className="pt-28 pb-14 bg-gradient-to-br from-primary to-primary-light">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div className="text-white" initial={{ opacity: 0, y: 26 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.55 }}>
<Breadcrumbs items={[{ label: locale === 'en' ? 'AI Assistant' : 'AI智能助手', href: '/assistant' }]} className="mb-5" />
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
<div>
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">
{locale === 'en' ? 'Chengyu AI Customer Service' : '诚裕智能客服'}
</h1>
<p className="mt-4 text-lg text-white/85 max-w-2xl leading-relaxed">
{locale === 'en'
? 'Knowledge-base powered Q&A, plus a fast lead channel for consulting.'
: '基于知识库的自然语言问答,同时也是诚裕集团重要的留资渠道。'}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/10 border border-white/20 text-sm">
<Sparkles size={16} />
{locale === 'en' ? 'Fast response' : '快速响应'}
</span>
<span className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/10 border border-white/20 text-sm">
<ClipboardList size={16} />
{locale === 'en' ? 'Lead capture' : '留资转化'}
</span>
</div>
</div>
</motion.div>
</div>
</section>
<section className="py-14 -mt-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-5 gap-8">
<div className="lg:col-span-3">
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 bg-white/70">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center">
<Bot size={20} />
</div>
<div>
<div className="font-semibold text-primary-dark">{locale === 'en' ? 'Chengyu AI Customer Service' : '诚裕智能客服'}</div>
<div className="text-xs text-gray-500">{locale === 'en' ? 'Knowledge-base powered answers' : '基于知识库内容检索与整理'}</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setActiveTab('chat')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'chat' ? 'bg-primary/5 text-primary' : 'text-gray-600 hover:bg-gray-50'
}`}
>
{locale === 'en' ? 'Chat' : '对话'}
</button>
<button
type="button"
onClick={() => setActiveTab('lead')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'lead' ? 'bg-primary/5 text-primary' : 'text-gray-600 hover:bg-gray-50'
}`}
>
{locale === 'en' ? 'Leave info' : '留资'}
</button>
</div>
</div>
{activeTab === 'chat' ? (
<>
<div ref={listRef} className="h-[460px] overflow-y-auto px-6 py-6 bg-gradient-to-b from-white to-background">
<div className="space-y-4">
{messages.map((m) => (
<div key={m.id} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div
className={`max-w-[85%] rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-sm border ${
m.role === 'user'
? 'bg-primary text-white border-primary/20'
: 'bg-white text-gray-800 border-gray-100'
}`}
>
<pre className="whitespace-pre-wrap font-sans">{m.content}</pre>
</div>
</div>
))}
{isReplying && (
<div className="flex justify-start">
<div className="max-w-[85%] rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-sm border bg-white text-gray-800 border-gray-100 flex items-center gap-2">
<Loader2 className="animate-spin" size={16} />
<span>{locale === 'en' ? 'Thinking…' : '正在检索并整理…'}</span>
</div>
</div>
)}
</div>
<div className="mt-6">
<div className="text-xs text-gray-500 mb-2">{locale === 'en' ? 'Try examples:' : '试试这些问题:'}</div>
<div className="flex flex-wrap gap-2">
{exampleQuestions.map((q) => (
<button
key={q}
type="button"
onClick={() => void replyTo(q)}
className="text-left px-3 py-2 rounded-xl bg-white border border-gray-100 hover:border-primary/30 hover:bg-primary/5 transition-colors text-sm text-gray-700"
>
{q}
</button>
))}
</div>
</div>
</div>
<div className="px-6 py-4 border-t border-gray-100 bg-white">
<div className="flex items-end gap-3">
<div className="flex-1">
<label htmlFor="assistantInput" className="sr-only">
{locale === 'en' ? 'Message input' : '输入问题'}
</label>
<textarea
id="assistantInput"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={locale === 'en' ? 'Type your question…' : '请输入你的问题…'}
rows={2}
className="w-full resize-none rounded-xl border border-gray-200 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
void handleSend()
}
}}
/>
</div>
<button
type="button"
onClick={() => void handleSend()}
disabled={!normalizeText(input) || isReplying}
className={`inline-flex items-center justify-center w-12 h-12 rounded-xl text-white transition-colors ${
!normalizeText(input) || isReplying ? 'bg-gray-300 cursor-not-allowed' : 'bg-primary hover:bg-primary-light'
}`}
aria-label={locale === 'en' ? 'Send' : '发送'}
>
<Send size={18} />
</button>
</div>
<p className="mt-2 text-xs text-gray-500">
{locale === 'en' ? 'Tip: Enter to send, Shift+Enter for a new line.' : '提示Enter 发送Shift+Enter 换行。'}
</p>
</div>
</>
) : (
<div className="px-6 py-6 bg-white">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-accent/15 text-accent-dark flex items-center justify-center">
<Phone size={18} />
</div>
<div className="flex-1">
<h2 className="text-lg font-semibold text-primary-dark">
{locale === 'en' ? 'Leave your information for a callback' : '填写信息,便于我们联系你'}
</h2>
<p className="mt-1 text-sm text-gray-600 leading-relaxed">
{locale === 'en'
? 'Phone number is required. The ops team can process submissions in the backend.'
: '手机号为必填项;提交后,后台运营人员可及时查看并处理(可在运营平台表单里配置邮件通知)。'}
</p>
</div>
</div>
{leadSubmitted && (
<div className="mt-5 p-4 rounded-xl border border-green-200 bg-green-50 flex items-start gap-3">
<CheckCircle size={20} className="text-green-600 mt-0.5" />
<div>
<div className="font-medium text-green-800">{locale === 'en' ? 'Submitted successfully' : '提交成功'}</div>
<div className="text-sm text-green-700 mt-1">
{leadFormId
? locale === 'en'
? 'Our team will contact you shortly.'
: '已收到你的信息,我们会尽快与你联系。'
: locale === 'en'
? 'Backend form not configured yet; saved locally for now.'
: '暂未找到后台表单配置,本次留资已临时保存在浏览器本地(请尽快在运营平台创建表单以正式留存)。'}
</div>
</div>
</div>
)}
{leadSubmitError && (
<div className="mt-5 p-4 rounded-xl border border-red-200 bg-red-50 text-sm text-red-700">
{leadSubmitError}
</div>
)}
<form onSubmit={submitLead} className="mt-6 space-y-5">
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label htmlFor="leadName" className="block text-sm font-medium text-gray-700 mb-2">
{locale === 'en' ? 'Name' : '姓名'}
</label>
<div className="relative">
<User size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
id="leadName"
value={lead.name}
onChange={(e) => {
setLead((p) => ({ ...p, name: e.target.value }))
if (leadErrors.name) setLeadErrors((p) => ({ ...p, name: undefined }))
}}
className="w-full rounded-xl border border-gray-200 pl-10 pr-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
placeholder={locale === 'en' ? 'Your name (optional)' : '你的姓名(选填)'}
autoComplete="name"
/>
</div>
{leadErrors.name && <p className="mt-1 text-xs text-red-600">{leadErrors.name}</p>}
</div>
<div>
<label htmlFor="leadPhone" className="block text-sm font-medium text-gray-700 mb-2">
{locale === 'en' ? 'Phone (required)' : '手机号(必填)'}
</label>
<div className="relative">
<Phone size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
id="leadPhone"
value={lead.phone}
onChange={(e) => {
setLead((p) => ({ ...p, phone: e.target.value }))
if (leadErrors.phone) setLeadErrors((p) => ({ ...p, phone: undefined }))
}}
className={`w-full rounded-xl border pl-10 pr-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 ${
leadErrors.phone ? 'border-red-300 focus:border-red-500' : 'border-gray-200 focus:border-primary'
}`}
placeholder={locale === 'en' ? '11-digit mobile' : '请输入 11 位手机号'}
inputMode="numeric"
autoComplete="tel"
/>
</div>
{leadErrors.phone && <p className="mt-1 text-xs text-red-600">{leadErrors.phone}</p>}
</div>
</div>
<div>
<label htmlFor="leadCompany" className="block text-sm font-medium text-gray-700 mb-2">
{locale === 'en' ? 'Company' : '公司'}
</label>
<div className="relative">
<MessageSquare size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
id="leadCompany"
value={lead.company}
onChange={(e) => {
setLead((p) => ({ ...p, company: e.target.value }))
if (leadErrors.company) setLeadErrors((p) => ({ ...p, company: undefined }))
}}
className="w-full rounded-xl border border-gray-200 pl-10 pr-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
placeholder={locale === 'en' ? 'Company name (optional)' : '公司名称(选填)'}
autoComplete="organization"
/>
</div>
{leadErrors.company && <p className="mt-1 text-xs text-red-600">{leadErrors.company}</p>}
</div>
<div>
<label htmlFor="leadDemand" className="block text-sm font-medium text-gray-700 mb-2">
{locale === 'en' ? 'Consulting needs' : '咨询需求'}
</label>
<textarea
id="leadDemand"
value={lead.demand}
onChange={(e) => {
setLead((p) => ({ ...p, demand: e.target.value }))
if (leadErrors.demand) setLeadErrors((p) => ({ ...p, demand: undefined }))
}}
rows={4}
className={`w-full resize-none rounded-xl border px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 ${
leadErrors.demand ? 'border-red-300 focus:border-red-500' : 'border-gray-200 focus:border-primary'
}`}
placeholder={locale === 'en' ? 'What do you want to consult about?' : '请描述你想咨询的问题(如:高新申报、体系搭建、审计口径…)'}
/>
{leadErrors.demand && <p className="mt-1 text-xs text-red-600">{leadErrors.demand}</p>}
</div>
<button
type="submit"
disabled={leadSubmitting}
className={`w-full rounded-xl py-3.5 text-sm font-medium text-white transition-colors inline-flex items-center justify-center gap-2 ${
leadSubmitting ? 'bg-gray-400 cursor-not-allowed' : 'bg-primary hover:bg-primary-light'
}`}
>
{leadSubmitting ? <Loader2 className="animate-spin" size={18} /> : <Send size={18} />}
{leadSubmitting ? (locale === 'en' ? 'Submitting…' : '提交中…') : locale === 'en' ? 'Submit' : '提交留资'}
</button>
<div className="text-xs text-gray-500 leading-relaxed">
{leadFormLoading ? (
<span>{locale === 'en' ? 'Loading backend form configuration…' : '正在加载后台表单配置…'}</span>
) : leadFormId ? (
<span>
{locale === 'en'
? 'This submission will be recorded in the operations platform.'
: '本次留资将记录到运营平台,便于运营人员及时查看处理。'}
</span>
) : (
<span>
{locale === 'en'
? 'Backend form not found. Please create a form titled "AI Assistant Lead".'
: '未找到后台留资表单。建议在运营平台创建标题包含“AI智能助手/留资”的表单,并配置通知(邮件/Webhook以实现“及时提醒”。'}
</span>
)}
</div>
</form>
</div>
)}
</div>
</div>
<aside className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<h2 className="text-lg font-semibold text-primary-dark flex items-center gap-2">
<Sparkles size={18} className="text-accent-dark" />
{locale === 'en' ? 'How it works' : '如何运行'}
</h2>
<ul className="mt-4 space-y-3 text-sm text-gray-700">
<li className="flex gap-2">
<span className="mt-0.5 w-6 h-6 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0">1</span>
<span>{locale === 'en' ? 'Ops team maintains the knowledge base in the backend.' : '运营平台导入/维护知识库内容(文章、条目、资料等)。'}</span>
</li>
<li className="flex gap-2">
<span className="mt-0.5 w-6 h-6 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0">2</span>
<span>{locale === 'en' ? 'Users ask questions; assistant searches and summarizes.' : '客户在前台自然语言提问,助手检索知识库并结构化回答。'}</span>
</li>
<li className="flex gap-2">
<span className="mt-0.5 w-6 h-6 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0">3</span>
<span>{locale === 'en' ? 'If manual follow-up is needed, guide users to leave contact info.' : '当无法确定答案或需进一步沟通时,引导客户留资(手机号必填并校验)。'}</span>
</li>
</ul>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<h2 className="text-lg font-semibold text-primary-dark">{locale === 'en' ? 'Lead channel focus' : '留资转化重点'}</h2>
<p className="mt-3 text-sm text-gray-700 leading-relaxed">
{locale === 'en'
? 'For Chengyu Group, the assistant is also an important acquisition channel.'
: '对诚裕集团而言,该助手不仅用于快速响应,更是重要的获客与留资渠道。'}
</p>
<div className="mt-4 grid gap-3">
<div className="rounded-xl border border-gray-100 bg-background p-4">
<div className="text-sm font-medium text-primary-dark">{locale === 'en' ? 'Required field' : '必填字段'}</div>
<div className="mt-1 text-xs text-gray-600">{locale === 'en' ? 'Phone number (11 digits) with basic validation.' : '手机号11 位)必填并进行基础校验。'}</div>
</div>
<div className="rounded-xl border border-gray-100 bg-background p-4">
<div className="text-sm font-medium text-primary-dark">{locale === 'en' ? 'Timely processing' : '及时提醒'}</div>
<div className="mt-1 text-xs text-gray-600">
{locale === 'en'
? 'Configure email/webhook notifications in the ops platform form settings.'
: '建议在运营平台表单里配置邮件/Webhook 通知,确保运营人员及时处理。'}
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
</section>
</main>
<Footer />
</motion.div>
)
}
export default Assistant