import { ApiOutlined, ClearOutlined, RobotOutlined, SendOutlined, SettingOutlined, StopOutlined, UserOutlined, } from '@ant-design/icons'; import { Alert, Avatar, Button, Drawer, Empty, Form, Input, Space, Spin, Tag, Tooltip, message, } from 'antd'; import { type FC, useEffect, useMemo, useRef, useState } from 'react'; import './index.less'; interface ChatMessage { id: string; role: 'system' | 'user' | 'assistant'; content: string; createdAt: string; } interface AiSettings { baseUrl: string; apiKey: string; model: string; } // v2:服务商切换为 TokenFree,旧版本地缓存不再复用,避免过期默认值残留 const SETTINGS_KEY = 'parking-ai-settings-v2'; const LEGACY_SETTINGS_KEY = 'parking-ai-settings'; const MESSAGES_KEY = 'parking-ai-messages'; const PROVIDER_HOME = 'https://www.tokenfree.com/'; const DEFAULT_SETTINGS: AiSettings = { baseUrl: 'https://www.tokenfree.com/v1', apiKey: '', model: 'kimi-k2.6', }; const loadSettings = (): AiSettings => { try { const raw = localStorage.getItem(SETTINGS_KEY); if (raw) return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) }; // 仅迁移用户在旧服务商下已填写的 API Key,地址/模型强制使用新默认值 const legacyRaw = localStorage.getItem(LEGACY_SETTINGS_KEY); if (legacyRaw) { const legacy = JSON.parse(legacyRaw) as Partial; const migrated = { ...DEFAULT_SETTINGS, apiKey: typeof legacy.apiKey === 'string' ? legacy.apiKey : '', }; localStorage.setItem(SETTINGS_KEY, JSON.stringify(migrated)); return migrated; } return DEFAULT_SETTINGS; } catch { return DEFAULT_SETTINGS; } }; const createMessage = ( role: ChatMessage['role'], content: string, ): ChatMessage => ({ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, role, content, createdAt: new Date().toLocaleString(), }); const AiChat: FC = () => { const [settings, setSettings] = useState(loadSettings); const [messages, setMessages] = useState(() => { try { return JSON.parse(localStorage.getItem(MESSAGES_KEY) || '[]'); } catch { return []; } }); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const [settingsForm] = Form.useForm(); const abortRef = useRef(null); const listRef = useRef(null); const normalizedBaseUrl = useMemo( () => settings.baseUrl.replace(/\/+$/, ''), [settings.baseUrl], ); useEffect(() => { document.title = 'AI对话 - 智慧停车管理平台'; }, []); useEffect(() => { localStorage.setItem(MESSAGES_KEY, JSON.stringify(messages)); listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: 'smooth', }); }, [messages]); const saveSettings = async () => { const values = await settingsForm.validateFields(); const next = { ...values, baseUrl: values.baseUrl.replace(/\/+$/, ''), }; setSettings(next); localStorage.setItem(SETTINGS_KEY, JSON.stringify(next)); setSettingsOpen(false); message.success('AI 接口配置已保存'); }; const testConnection = async () => { const values = settingsForm.getFieldsValue(); const baseUrl = values.baseUrl.replace(/\/+$/, ''); if (!baseUrl) { message.warning('请先填写 API Base URL'); return; } try { const res = await fetch(`${baseUrl}/models`, { headers: { Authorization: `Bearer ${values.apiKey}` }, }); if (!res.ok) throw new Error(res.statusText); message.success('连接测试成功'); } catch (error) { message.error(`连接失败:${String(error)}`); } }; const stopGenerating = () => { abortRef.current?.abort(); setLoading(false); }; const sendMessage = async () => { const content = input.trim(); if (!content) return; if (!settings.apiKey) { setSettingsOpen(true); message.warning('请先配置 API Key'); return; } const userMessage = createMessage('user', content); const assistantMessage = createMessage('assistant', ''); const nextMessages = [...messages, userMessage, assistantMessage]; setMessages(nextMessages); setInput(''); setLoading(true); const controller = new AbortController(); abortRef.current = controller; const payload = { model: settings.model, stream: true, messages: [ { role: 'system', content: '你是智慧停车管理平台AI助手,可解答停车运营、设备状态、异常报警、园区监控和数据分析相关问题。', }, ...messages .filter((item) => item.role !== 'system') .map((item) => ({ role: item.role, content: item.content })), { role: 'user', content }, ], }; try { const response = await fetch(`${normalizedBaseUrl}/chat/completions`, { method: 'POST', signal: controller.signal, headers: { Authorization: `Bearer ${settings.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); if (!response.ok || !response.body) { throw new Error(response.statusText || 'AI 服务响应异常'); } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; let answer = ''; const patchLastMessage = (text: string) => { setMessages((current) => current.map((item, index) => index === current.length - 1 ? { ...item, content: text } : item, ), ); }; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const chunks = buffer.split('\n\n'); buffer = chunks.pop() || ''; chunks.forEach((chunk) => { chunk .split('\n') .filter((line) => line.startsWith('data:')) .forEach((line) => { const data = line.replace(/^data:\s*/, '').trim(); if (!data || data === '[DONE]') return; try { const json = JSON.parse(data); const delta = json.choices?.[0]?.delta?.content || ''; answer += delta; patchLastMessage(answer); } catch { // 忽略心跳或非标准分片 } }); }); } if (!answer) patchLastMessage('暂未收到有效回复,请稍后重试。'); } catch (error) { const text = error instanceof DOMException && error.name === 'AbortError' ? answerOrStopped() : `AI 服务连接失败:${String(error)}`; setMessages((current) => current.map((item, index) => index === current.length - 1 ? { ...item, content: text } : item, ), ); } finally { setLoading(false); abortRef.current = null; } }; const answerOrStopped = () => '已停止生成。'; const clearHistory = () => { setMessages([]); localStorage.removeItem(MESSAGES_KEY); message.success('对话记录已清空'); }; const openSettings = () => { settingsForm.setFieldsValue(settings); setSettingsOpen(true); }; return (
} size="large" />

智慧停车 AI 助手

{settings.model} 流式对话已就绪

{!settings.apiKey && ( 立即配置 } /> )}
{messages.length === 0 ? ( ) : ( messages.map((item) => (
: } />
{item.content || (loading ? : '')}
{item.createdAt}
)) )}
setInput(event.target.value)} onPressEnter={(event) => { if (!event.shiftKey) { event.preventDefault(); void sendMessage(); } }} /> {loading ? ( ) : ( )}
AI 接口配置 } open={settingsOpen} onClose={() => setSettingsOpen(false)} width={380} extra={ } >

TokenFree 使用教程

TokenFree 是统一 AI API 网关,支持标准 Chat Completions 接口协议 (流式输出 / Models 列表),注册后即可调用 Kimi、Qwen、Claude 等多家模型。

  1. 打开{' '} https://www.tokenfree.com/ ,点击注册并完成邮箱验证后登录。
  2. 登录后进入控制台「令牌 / API Keys」页面,点击「添加令牌」, 复制生成的 Key(以 sk- 开头)。
  3. 将 API Key 粘贴到下方表单;接口地址与默认模型已预填, 一般无需修改。
  4. 点击「测试连接」验证 Key 是否有效,通过后点击「保存」, 返回页面即可开始对话。

接口地址:https://www.tokenfree.com/v1 默认模型:kimi-k2.6

提示:也可在「模型 / Model」处填写 TokenFree 支持的其他模型名称; Key 仅保存在本地浏览器,不会上传到本系统服务器。

); }; export default AiChat;