文件
OmniCard/admin/src/pages/ai/concierge/index.tsx

674 行
21 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
Card,
Form,
Input,
Select,
Slider,
Switch,
Radio,
Button,
Space,
Message,
Spin,
Grid,
Tag,
Divider,
} from '@arco-design/web-react';
import axios from 'axios';
import useLocale from '@/utils/useLocale';
import { lightColors, spacing, radius } from '@/style/tokens';
import type {
AiConciergeConfig,
ConciergePersonaType,
ConciergeToneStyle,
DeepSeekConfig,
DeepSeekTestConnectionResult,
} from '@/types/omni';
import locale from './locale';
const { Row, Col } = Grid;
const GET_CONFIG_API = '/api/admin/ai/concierge/getByCardId';
const SAVE_CONFIG_API = '/api/admin/ai/concierge/saveByCardId';
const CARD_PAGE_API = '/api/admin/card/info/page';
const DEEPSEEK_CONFIG_API = '/api/admin/ai/deepseek/config';
const DEEPSEEK_TEST_API = '/api/admin/ai/deepseek/test-connection';
interface CoolResponse<T> {
code: number;
message?: string;
data?: T;
}
interface CardOptionItem {
label: string;
value: number;
}
interface CardPageRecord {
id: number;
name: string;
title?: string;
}
interface CardPageResult {
list?: CardPageRecord[];
}
interface PersonaPresetDetail {
welcomeMsg: string;
promptTemplate: string;
toneStyle: ConciergeToneStyle;
triggerLeadThreshold: number;
}
const PRESET_DATA: Record<ConciergePersonaType, PersonaPresetDetail> = {
trade_expert: {
welcomeMsg:
'您好!我是您的专属出海业务顾问。请问有什么全球贸易或合作需求可以协助您?',
promptTemplate:
'你是一名深耕国际贸易与跨国商务的出海业务专家。用专业、沉稳的商务语调为访客解答产品出海、跨境结算、供应链合作等咨询。引导有明确意向的客商提供联系方式。',
toneStyle: 'professional',
triggerLeadThreshold: 70,
},
business_rep: {
welcomeMsg: '欢迎光临!我是商务联络代表,很高兴为您服务。',
promptTemplate:
'你是一名高效热情的商务拓展代表。重点介绍公司业务优势与成功案例,积极促成商业联络,敏锐捕捉商机。',
toneStyle: 'enthusiastic',
triggerLeadThreshold: 60,
},
tech_advisor: {
welcomeMsg: '您好,我是技术顾问,随时解答系统对接与技术架构问题。',
promptTemplate:
'你是一名资深解决方案架构师与技术顾问。精确、客观、条理清晰地回答技术规格、API 集成与实施流程问题。',
toneStyle: 'direct',
triggerLeadThreshold: 80,
},
custom: {
welcomeMsg: '',
promptTemplate: '',
toneStyle: 'professional',
triggerLeadThreshold: 70,
},
};
const DEFAULT_CARD_ID = 1;
export default function AiConciergePage(): JSX.Element {
const t = useLocale(locale);
const [form] = Form.useForm<AiConciergeConfig>();
const [loading, setLoading] = useState<boolean>(false);
const [saving, setSaving] = useState<boolean>(false);
const [selectedCardId, setSelectedCardId] = useState<number>(DEFAULT_CARD_ID);
const [cardOptions, setCardOptions] = useState<CardOptionItem[]>([]);
const [deepSeekForm] = Form.useForm<DeepSeekConfig>();
const [deepSeekLoading, setDeepSeekLoading] = useState<boolean>(false);
const [deepSeekSaving, setDeepSeekSaving] = useState<boolean>(false);
const [deepSeekTesting, setDeepSeekTesting] = useState<boolean>(false);
const [deepSeekConfig, setDeepSeekConfig] = useState<DeepSeekConfig | null>(null);
const [testResult, setTestResult] = useState<DeepSeekTestConnectionResult | null>(null);
const loadDeepSeekConfig = async (): Promise<void> => {
setDeepSeekLoading(true);
try {
const res = await axios.post<CoolResponse<DeepSeekConfig>>(
DEEPSEEK_CONFIG_API,
{}
);
if (res.data && res.data.code === 1000 && res.data.data) {
const d = res.data.data;
setDeepSeekConfig(d);
deepSeekForm.setFieldsValue({
apiKey: d.apiKey || '',
baseUrl: d.baseUrl || 'https://api.deepseek.com',
model: d.model || 'deepseek-chat',
temperature: d.temperature ?? 0.7,
maxTokens: d.maxTokens ?? 2048,
enabled: d.enabled !== false,
});
}
} catch {
// 保持静默优雅降级
} finally {
setDeepSeekLoading(false);
}
};
const handleTestDeepSeek = async (): Promise<void> => {
setDeepSeekTesting(true);
setTestResult(null);
try {
const values = deepSeekForm.getFieldsValue();
const res = await axios.post<CoolResponse<DeepSeekTestConnectionResult>>(
DEEPSEEK_TEST_API,
{ apiKey: values.apiKey }
);
if (res.data && res.data.code === 1000 && res.data.data) {
setTestResult(res.data.data);
if (res.data.data.success) {
Message.success(
`${t['aiDeepSeek.test.success']} (${res.data.data.latencyMs ?? 0}ms)`
);
} else {
Message.error(res.data.data.message || t['aiDeepSeek.test.fail']);
}
} else {
Message.error(res.data?.message || t['aiDeepSeek.test.fail']);
}
} catch {
Message.error(t['aiDeepSeek.test.fail']);
} finally {
setDeepSeekTesting(false);
}
};
const handleSaveDeepSeek = async (values: DeepSeekConfig): Promise<void> => {
setDeepSeekSaving(true);
try {
const payload: Partial<DeepSeekConfig> = {
baseUrl: values.baseUrl,
model: values.model,
temperature: Number(values.temperature),
maxTokens: Number(values.maxTokens),
enabled: Boolean(values.enabled),
};
if (values.apiKey && !values.apiKey.includes('****')) {
payload.apiKey = values.apiKey;
}
const res = await axios.post<CoolResponse<DeepSeekConfig>>(
DEEPSEEK_CONFIG_API,
payload
);
if (res.data && res.data.code === 1000 && res.data.data) {
Message.success(t['aiDeepSeek.save.success']);
setDeepSeekConfig(res.data.data);
if (res.data.data.apiKey) {
deepSeekForm.setFieldValue('apiKey', res.data.data.apiKey);
}
} else {
Message.error(res.data?.message || t['aiDeepSeek.save.fail']);
}
} catch {
Message.error(t['aiDeepSeek.save.fail']);
} finally {
setDeepSeekSaving(false);
}
};
const fetchCardOptions = async (): Promise<void> => {
try {
const res = await axios.post<CoolResponse<CardPageResult>>(
CARD_PAGE_API,
{
page: 1,
size: 20,
}
);
if (res.data && res.data.code === 1000 && res.data.data?.list) {
const options = res.data.data.list.map((c) => ({
label: `${c.name || 'Card'} (#${c.id})`,
value: Number(c.id),
}));
setCardOptions(options);
}
} catch {
// 保持静默降级,允许用户手动输入或保留默认名片
}
};
const loadConciergeConfig = async (cardId: number): Promise<void> => {
setLoading(true);
try {
const res = await axios.post<CoolResponse<AiConciergeConfig>>(
GET_CONFIG_API,
{
cardId,
}
);
if (res.data && res.data.code === 1000 && res.data.data) {
const d = res.data.data;
form.setFieldsValue({
cardId: Number(d.cardId),
personaType: d.personaType || 'trade_expert',
welcomeMsg: d.welcomeMsg,
triggerLeadThreshold: d.triggerLeadThreshold ?? 70,
promptTemplate: d.promptTemplate,
toneStyle: d.toneStyle || 'professional',
isEnabled: d.isEnabled !== false,
});
} else {
const fallback = PRESET_DATA.trade_expert;
form.setFieldsValue({
cardId,
personaType: 'trade_expert',
welcomeMsg: fallback.welcomeMsg,
triggerLeadThreshold: fallback.triggerLeadThreshold,
promptTemplate: fallback.promptTemplate,
toneStyle: fallback.toneStyle,
isEnabled: true,
});
}
} catch {
Message.error(t['aiConcierge.request.fail']);
const fallback = PRESET_DATA.trade_expert;
form.setFieldsValue({
cardId,
personaType: 'trade_expert',
welcomeMsg: fallback.welcomeMsg,
triggerLeadThreshold: fallback.triggerLeadThreshold,
promptTemplate: fallback.promptTemplate,
toneStyle: fallback.toneStyle,
isEnabled: true,
});
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCardOptions();
loadConciergeConfig(selectedCardId);
loadDeepSeekConfig();
}, []);
const handleCardChange = (cardId: number): void => {
setSelectedCardId(cardId);
loadConciergeConfig(cardId);
};
const handlePersonaChange = (type: ConciergePersonaType): void => {
if (type !== 'custom') {
const preset = PRESET_DATA[type];
form.setFieldsValue({
personaType: type,
welcomeMsg: preset.welcomeMsg,
promptTemplate: preset.promptTemplate,
toneStyle: preset.toneStyle,
triggerLeadThreshold: preset.triggerLeadThreshold,
});
} else {
form.setFieldsValue({ personaType: type });
}
};
const handleSave = async (values: AiConciergeConfig): Promise<void> => {
setSaving(true);
try {
const payload: Partial<AiConciergeConfig> = {
cardId: Number(values.cardId),
personaType: values.personaType,
welcomeMsg: values.welcomeMsg,
triggerLeadThreshold: Number(values.triggerLeadThreshold),
promptTemplate: values.promptTemplate,
toneStyle: values.toneStyle,
isEnabled: Boolean(values.isEnabled),
};
const res = await axios.post<CoolResponse<AiConciergeConfig>>(
SAVE_CONFIG_API,
payload
);
if (res.data && res.data.code === 1000) {
Message.success(t['aiConcierge.request.success']);
} else {
Message.error(res.data?.message || t['aiConcierge.request.saveFail']);
}
} catch {
Message.error(t['aiConcierge.request.saveFail']);
} finally {
setSaving(false);
}
};
const sliderMarks = {
0: '0',
50: '50',
70: '70',
90: '90',
100: '100',
};
return (
<div>
<Card
title={t['aiConcierge.card.title']}
bordered={false}
style={{
borderRadius: radius.card,
border: `1px solid ${lightColors.border}`,
backgroundColor: lightColors.bg,
}}
>
<div
style={{
color: lightColors.text2,
marginBottom: spacing[24],
fontSize: '14px',
}}
>
{t['aiConcierge.card.desc']}
</div>
<Spin loading={loading} style={{ width: '100%' }}>
<Form
form={form}
layout="vertical"
onSubmit={handleSave}
initialValues={{
cardId: DEFAULT_CARD_ID,
personaType: 'trade_expert',
toneStyle: 'professional',
triggerLeadThreshold: 70,
isEnabled: true,
}}
>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t['aiConcierge.form.cardSelect']}
field="cardId"
rules={[
{
required: true,
message: t['aiConcierge.validation.cardId'],
},
]}
>
<Select
placeholder={t['aiConcierge.form.cardSelect.placeholder']}
onChange={handleCardChange}
options={
cardOptions.length > 0
? cardOptions
: [{ label: `Default Card (#${DEFAULT_CARD_ID})`, value: DEFAULT_CARD_ID }]
}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t['aiConcierge.form.status']}
field="isEnabled"
triggerPropName="checked"
extra={t['aiConcierge.form.status.desc']}
>
<Switch />
</Form.Item>
</Col>
</Row>
<Form.Item
label={t['aiConcierge.form.persona']}
field="personaType"
>
<Radio.Group
type="button"
onChange={handlePersonaChange}
style={{ width: '100%' }}
>
<Radio value="trade_expert">
{t['aiConcierge.persona.trade_expert']}
</Radio>
<Radio value="business_rep">
{t['aiConcierge.persona.business_rep']}
</Radio>
<Radio value="tech_advisor">
{t['aiConcierge.persona.tech_advisor']}
</Radio>
<Radio value="custom">{t['aiConcierge.persona.custom']}</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label={t['aiConcierge.form.tone']}
field="toneStyle"
>
<Select>
<Select.Option value="professional">
{t['aiConcierge.tone.professional']}
</Select.Option>
<Select.Option value="enthusiastic">
{t['aiConcierge.tone.enthusiastic']}
</Select.Option>
<Select.Option value="direct">
{t['aiConcierge.tone.direct']}
</Select.Option>
<Select.Option value="friendly">
{t['aiConcierge.tone.friendly']}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t['aiConcierge.form.welcome']}
field="welcomeMsg"
rules={[
{
required: true,
message: t['aiConcierge.validation.welcome'],
},
]}
>
<Input.TextArea
rows={3}
placeholder={t['aiConcierge.form.welcome.placeholder']}
/>
</Form.Item>
<Form.Item
label={t['aiConcierge.form.prompt']}
field="promptTemplate"
>
<Input.TextArea
rows={4}
placeholder={t['aiConcierge.form.prompt.placeholder']}
/>
</Form.Item>
<Form.Item
label={t['aiConcierge.form.threshold']}
field="triggerLeadThreshold"
extra={t['aiConcierge.form.threshold.desc']}
>
<Slider
min={0}
max={100}
marks={sliderMarks}
style={{ width: '100%', maxWidth: '560px' }}
/>
</Form.Item>
<div style={{ marginTop: spacing[32] }}>
<Space>
<Button
type="primary"
htmlType="submit"
loading={saving}
style={{ borderRadius: radius.button }}
>
{t['aiConcierge.action.save']}
</Button>
</Space>
</div>
</Form>
</Spin>
</Card>
<Card
title={
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
<span>{t['aiDeepSeek.card.title']}</span>
<Tag color={deepSeekConfig?.isConfigured ? 'green' : 'gray'}>
{deepSeekConfig?.isConfigured
? t['aiDeepSeek.status.configured']
: t['aiDeepSeek.status.unconfigured']}
</Tag>
</div>
}
bordered={false}
style={{
marginTop: spacing[24],
borderRadius: radius.card,
border: `1px solid ${lightColors.border}`,
backgroundColor: lightColors.bg,
}}
>
<div
style={{
color: lightColors.text2,
marginBottom: spacing[24],
fontSize: '14px',
}}
>
{t['aiDeepSeek.card.desc']}
</div>
<Spin loading={deepSeekLoading} style={{ width: '100%' }}>
<Form
form={deepSeekForm}
layout="vertical"
onSubmit={handleSaveDeepSeek}
initialValues={{
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
temperature: 0.7,
maxTokens: 2048,
enabled: true,
}}
>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.apiKey']}
field="apiKey"
extra={
deepSeekConfig?.apiKey
? `已保存密钥: ${deepSeekConfig.apiKey}`
: undefined
}
>
<Input.Password
placeholder={t['aiDeepSeek.form.apiKey.placeholder']}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.status']}
field="enabled"
triggerPropName="checked"
extra={t['aiDeepSeek.form.status.desc']}
>
<Switch />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.baseUrl']}
field="baseUrl"
rules={[{ required: true }]}
>
<Input placeholder="https://api.deepseek.com" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.model']}
field="model"
rules={[{ required: true }]}
>
<Select>
<Select.Option value="deepseek-chat">
{t['aiDeepSeek.model.chat']}
</Select.Option>
<Select.Option value="deepseek-reasoner">
{t['aiDeepSeek.model.reasoner']}
</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t['aiDeepSeek.form.temperature']}
field="temperature"
extra={t['aiDeepSeek.form.temperature.desc']}
>
<Slider
min={0}
max={1.5}
step={0.1}
marks={{ 0: '0', 0.7: '0.7', 1.0: '1.0', 1.5: '1.5' }}
style={{ width: '100%' }}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Max Tokens"
field="maxTokens"
>
<Select>
<Select.Option value={1024}>1024</Select.Option>
<Select.Option value={2048}>2048</Select.Option>
<Select.Option value={4096}>4096</Select.Option>
<Select.Option value={8192}>8192</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
{testResult && (
<div style={{ marginBottom: spacing[16] }}>
<Tag
color={testResult.success ? 'green' : 'red'}
style={{ padding: `${spacing[4]} ${spacing[12]}` }}
>
{testResult.message || (testResult.success ? '连通成功' : '连通失败')}
{testResult.latencyMs !== undefined && ` | ${testResult.latencyMs}ms`}
{testResult.model && ` | ${testResult.model}`}
</Tag>
</div>
)}
<div style={{ marginTop: spacing[24] }}>
<Space>
<Button
htmlType="submit"
loading={deepSeekSaving}
style={{ borderRadius: radius.button }}
>
{t['aiDeepSeek.action.save']}
</Button>
<Button
type="outline"
onClick={handleTestDeepSeek}
loading={deepSeekTesting}
style={{ borderRadius: radius.button }}
>
{t['aiDeepSeek.action.test']}
</Button>
</Space>
</div>
</Form>
</Spin>
</Card>
</div>
);
}