800 lines
33 KiB
TypeScript
800 lines
33 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
||
import { adminAPI } from '@/api';
|
||
import './Apps.css';
|
||
|
||
interface AIApp {
|
||
id: string;
|
||
name: string;
|
||
description: string | null;
|
||
type: string;
|
||
category: string;
|
||
tags: string | null;
|
||
icon: string | null;
|
||
status: string;
|
||
version: number;
|
||
is_template: boolean;
|
||
is_public: boolean;
|
||
config: any;
|
||
created_at: string | null;
|
||
updated_at: string | null;
|
||
published_at: string | null;
|
||
analytics_7d?: any;
|
||
versions?: any[];
|
||
}
|
||
|
||
interface AppStats {
|
||
total: number;
|
||
published: number;
|
||
draft: number;
|
||
paused: number;
|
||
archived: number;
|
||
templates: number;
|
||
by_type: Record<string, number>;
|
||
by_category: Record<string, number>;
|
||
recent_7d: number;
|
||
total_versions: number;
|
||
}
|
||
|
||
interface Template {
|
||
id: string;
|
||
name: string;
|
||
description: string | null;
|
||
type: string;
|
||
category: string;
|
||
icon: string | null;
|
||
config: any;
|
||
created_at: string | null;
|
||
}
|
||
|
||
const CATEGORY_NAMES: Record<string, string> = {
|
||
customer_service: '客服助手',
|
||
data_analysis: '数据分析',
|
||
coding: '编程辅助',
|
||
writing: '内容创作',
|
||
education: '教育培训',
|
||
marketing: '营销推广',
|
||
hr: '人力资源',
|
||
finance: '财务法务',
|
||
custom: '自定义',
|
||
};
|
||
|
||
const TYPE_NAMES: Record<string, string> = {
|
||
chatbot: '聊天机器人',
|
||
assistant: 'AI助手',
|
||
workflow: '工作流',
|
||
api_service: 'API服务',
|
||
};
|
||
|
||
const STATUS_COLORS: Record<string, string> = {
|
||
draft: '#f59e0b',
|
||
published: '#10b981',
|
||
paused: '#6b7280',
|
||
archived: '#9ca3af',
|
||
};
|
||
|
||
const STATUS_NAMES: Record<string, string> = {
|
||
draft: '草稿',
|
||
published: '已发布',
|
||
paused: '已暂停',
|
||
archived: '已归档',
|
||
};
|
||
|
||
export default function AdminApps() {
|
||
const [activeTab, setActiveTab] = useState<'apps' | 'templates' | 'analytics'>('apps');
|
||
const [stats, setStats] = useState<AppStats | null>(null);
|
||
const [apps, setApps] = useState<AIApp[]>([]);
|
||
const [templates, setTemplates] = useState<Template[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [search, setSearch] = useState('');
|
||
const [filterType, setFilterType] = useState('');
|
||
const [filterStatus, setFilterStatus] = useState('');
|
||
const [filterCategory, setFilterCategory] = useState('');
|
||
const [page, setPage] = useState(1);
|
||
const [total, setTotal] = useState(0);
|
||
const [totalPages, setTotalPages] = useState(1);
|
||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||
const [editApp, setEditApp] = useState<AIApp | null>(null);
|
||
const [detailApp, setDetailApp] = useState<AIApp | null>(null);
|
||
const [detailAnalytics, setDetailAnalytics] = useState<any>(null);
|
||
const [showTemplateModal, setShowTemplateModal] = useState(false);
|
||
|
||
const fetchStats = useCallback(async () => {
|
||
try {
|
||
const res = await adminAPI.getAIAppStats();
|
||
if (res.code === 0) setStats(res.data);
|
||
} catch (e) { console.error(e); }
|
||
}, []);
|
||
|
||
const fetchApps = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const params = new URLSearchParams();
|
||
params.set('page', String(page));
|
||
params.set('page_size', '20');
|
||
if (search) params.set('search', search);
|
||
if (filterType) params.set('app_type', filterType);
|
||
if (filterStatus) params.set('status', filterStatus);
|
||
if (filterCategory) params.set('category', filterCategory);
|
||
const res = await adminAPI.getAIApps(params.toString());
|
||
if (res.code === 0) {
|
||
setApps(res.data.items || []);
|
||
setTotal(res.data.total || 0);
|
||
setTotalPages(res.data.total_pages || 1);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
finally { setLoading(false); }
|
||
}, [page, search, filterType, filterStatus, filterCategory]);
|
||
|
||
const fetchTemplates = useCallback(async () => {
|
||
try {
|
||
const res = await adminAPI.getAIAppTemplates();
|
||
if (res.code === 0) setTemplates(res.data.items || []);
|
||
} catch (e) { console.error(e); }
|
||
}, []);
|
||
|
||
const fetchDetail = useCallback(async (id: string) => {
|
||
try {
|
||
const res = await adminAPI.getAIApp(id);
|
||
if (res.code === 0) {
|
||
setDetailApp(res.data);
|
||
// Fetch analytics
|
||
try {
|
||
const aRes = await adminAPI.getAIAppAnalytics(id);
|
||
if (aRes.code === 0) setDetailAnalytics(aRes.data);
|
||
} catch (e) { /* analytics may be empty */ }
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchStats();
|
||
}, [fetchStats]);
|
||
|
||
useEffect(() => {
|
||
if (activeTab === 'apps') fetchApps();
|
||
if (activeTab === 'templates') fetchTemplates();
|
||
}, [activeTab, fetchApps, fetchTemplates]);
|
||
|
||
const handleCreate = async (data: any) => {
|
||
try {
|
||
const res = await adminAPI.createAIApp(data);
|
||
if (res.code === 0) {
|
||
setShowCreateModal(false);
|
||
fetchApps();
|
||
fetchStats();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
};
|
||
|
||
const handleUpdate = async (data: any) => {
|
||
if (!editApp) return;
|
||
try {
|
||
const res = await adminAPI.updateAIApp(editApp.id, data);
|
||
if (res.code === 0) {
|
||
setEditApp(null);
|
||
fetchApps();
|
||
fetchStats();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
};
|
||
|
||
const handleDelete = async (id: string) => {
|
||
if (!confirm('确定要删除此应用吗?此操作不可逆。')) return;
|
||
try {
|
||
const res = await adminAPI.deleteAIApp(id);
|
||
if (res.code === 0) {
|
||
fetchApps();
|
||
fetchStats();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
};
|
||
|
||
const handleLifecycle = async (id: string, action: 'publish' | 'pause' | 'archive' | 'duplicate') => {
|
||
try {
|
||
const res = await adminAPI.aiAppLifecycle(id, action);
|
||
if (res.code === 0) {
|
||
fetchApps();
|
||
fetchStats();
|
||
if (detailApp?.id === id) fetchDetail(id);
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
};
|
||
|
||
const handleCreateTemplate = async (data: any) => {
|
||
try {
|
||
const res = await adminAPI.createAIAppTemplate(data);
|
||
if (res.code === 0) {
|
||
setShowTemplateModal(false);
|
||
fetchTemplates();
|
||
fetchStats();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
};
|
||
|
||
const handleFromTemplate = async (templateId: string, name: string) => {
|
||
try {
|
||
const res = await adminAPI.createAIAppFromTemplate(templateId, { name });
|
||
if (res.code === 0) {
|
||
setActiveTab('apps');
|
||
fetchApps();
|
||
fetchStats();
|
||
}
|
||
} catch (e) { console.error(e); }
|
||
};
|
||
|
||
const formatDate = (dt: string | null) => {
|
||
if (!dt) return '-';
|
||
try {
|
||
const d = new Date(dt);
|
||
return d.toLocaleDateString('zh-CN') + ' ' + d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||
} catch { return dt; }
|
||
};
|
||
|
||
return (
|
||
<div className="admin-apps-page">
|
||
<div className="admin-page-header">
|
||
<h1>AI应用管理</h1>
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
{activeTab === 'apps' && (
|
||
<button className="btn-primary" onClick={() => setShowCreateModal(true)}>+ 创建应用</button>
|
||
)}
|
||
{activeTab === 'templates' && (
|
||
<button className="btn-primary" onClick={() => setShowTemplateModal(true)}>+ 创建模板</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats Cards */}
|
||
{stats && (
|
||
<div className="stats-grid">
|
||
<div className="stat-card">
|
||
<div className="stat-value">{stats.total}</div>
|
||
<div className="stat-label">应用总数</div>
|
||
</div>
|
||
<div className="stat-card">
|
||
<div className="stat-value" style={{ color: '#10b981' }}>{stats.published}</div>
|
||
<div className="stat-label">已发布</div>
|
||
</div>
|
||
<div className="stat-card">
|
||
<div className="stat-value" style={{ color: '#f59e0b' }}>{stats.draft}</div>
|
||
<div className="stat-label">草稿</div>
|
||
</div>
|
||
<div className="stat-card">
|
||
<div className="stat-value">{stats.templates}</div>
|
||
<div className="stat-label">应用模板</div>
|
||
</div>
|
||
<div className="stat-card">
|
||
<div className="stat-value" style={{ color: '#3b82f6' }}>{stats.recent_7d}</div>
|
||
<div className="stat-label">近7天新增</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Tabs */}
|
||
<div className="tabs-bar">
|
||
{([
|
||
{ key: 'apps' as const, label: '应用列表', count: stats?.total },
|
||
{ key: 'templates' as const, label: '应用模板', count: stats?.templates },
|
||
{ key: 'analytics' as const, label: '分析概览', count: undefined as number | undefined },
|
||
]).map(tab => (
|
||
<button
|
||
key={tab.key}
|
||
className={`tab-btn ${activeTab === tab.key ? 'active' : ''}`}
|
||
onClick={() => setActiveTab(tab.key)}
|
||
>
|
||
{tab.label}
|
||
{tab.count !== undefined && tab.count > 0 && (
|
||
<span className="tab-badge">{tab.count}</span>
|
||
)} </button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Apps Tab */}
|
||
{activeTab === 'apps' && (
|
||
<div>
|
||
{/* Filters */}
|
||
<div className="filter-bar">
|
||
<input
|
||
type="text"
|
||
placeholder="搜索应用名称..."
|
||
value={search}
|
||
onChange={e => { setSearch(e.target.value); setPage(1); }}
|
||
className="filter-input"
|
||
/>
|
||
<select value={filterType} onChange={e => { setFilterType(e.target.value); setPage(1); }} className="filter-select">
|
||
<option value="">全部类型</option>
|
||
{Object.entries(TYPE_NAMES).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</select>
|
||
<select value={filterStatus} onChange={e => { setFilterStatus(e.target.value); setPage(1); }} className="filter-select">
|
||
<option value="">全部状态</option>
|
||
{Object.entries(STATUS_NAMES).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</select>
|
||
<select value={filterCategory} onChange={e => { setFilterCategory(e.target.value); setPage(1); }} className="filter-select">
|
||
<option value="">全部分类</option>
|
||
{Object.entries(CATEGORY_NAMES).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<table className="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>应用名称</th>
|
||
<th>类型</th>
|
||
<th>分类</th>
|
||
<th>状态</th>
|
||
<th>版本</th>
|
||
<th>创建时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{loading ? (
|
||
<tr><td colSpan={7} style={{ textAlign: 'center', padding: '40px' }}>加载中...</td></tr>
|
||
) : apps.length === 0 ? (
|
||
<tr><td colSpan={7} style={{ textAlign: 'center', padding: '40px', color: 'var(--color-text-tertiary)' }}>暂无应用</td></tr>
|
||
) : apps.map(app => (
|
||
<tr key={app.id} style={{ cursor: 'pointer' }} onClick={() => fetchDetail(app.id)}>
|
||
<td>
|
||
<div style={{ fontWeight: 500 }}>{app.name}</div>
|
||
{app.description && <div style={{ fontSize: '12px', color: 'var(--color-text-tertiary)', maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{app.description}</div>}
|
||
</td>
|
||
<td><span className="badge">{TYPE_NAMES[app.type] || app.type}</span></td>
|
||
<td>{CATEGORY_NAMES[app.category] || app.category}</td>
|
||
<td>
|
||
<span className="status-badge" style={{ background: STATUS_COLORS[app.status] + '20', color: STATUS_COLORS[app.status] }}>
|
||
{STATUS_NAMES[app.status] || app.status}
|
||
</span>
|
||
</td>
|
||
<td>v{app.version}</td>
|
||
<td style={{ fontSize: '13px', color: 'var(--color-text-secondary)' }}>{formatDate(app.created_at)}</td>
|
||
<td onClick={e => e.stopPropagation()}>
|
||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||
<button className="btn-sm" onClick={() => setEditApp(app)}>编辑</button>
|
||
{app.status === 'draft' && <button className="btn-sm btn-success" onClick={() => handleLifecycle(app.id, 'publish')}>发布</button>}
|
||
{app.status === 'published' && <button className="btn-sm" onClick={() => handleLifecycle(app.id, 'pause')}>暂停</button>}
|
||
{app.status === 'paused' && <button className="btn-sm btn-success" onClick={() => handleLifecycle(app.id, 'publish')}>恢复</button>}
|
||
<button className="btn-sm" onClick={() => handleLifecycle(app.id, 'duplicate')}>复制</button>
|
||
<button className="btn-sm btn-danger" onClick={() => handleDelete(app.id)}>删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
|
||
{/* Pagination */}
|
||
{totalPages > 1 && (
|
||
<div className="pagination">
|
||
<button disabled={page <= 1} onClick={() => setPage(p => p - 1)}>上一页</button>
|
||
<span>第 {page} / {totalPages} 页 (共 {total} 条)</span>
|
||
<button disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>下一页</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Templates Tab */}
|
||
{activeTab === 'templates' && (
|
||
<div className="templates-grid">
|
||
{templates.length === 0 ? (
|
||
<div style={{ textAlign: 'center', padding: '60px', color: 'var(--color-text-tertiary)' }}>
|
||
暂无模板,点击右上角"创建模板"添加
|
||
</div>
|
||
) : templates.map(tpl => (
|
||
<div key={tpl.id} className="template-card">
|
||
<div className="template-icon">{tpl.icon || '📦'}</div>
|
||
<div className="template-name">{tpl.name}</div>
|
||
<div className="template-desc">{tpl.description || '暂无描述'}</div>
|
||
<div className="template-meta">
|
||
<span className="badge">{TYPE_NAMES[tpl.type] || tpl.type}</span>
|
||
<span className="badge">{CATEGORY_NAMES[tpl.category] || tpl.category}</span>
|
||
</div>
|
||
<button className="btn-primary btn-block" onClick={() => {
|
||
const name = prompt('请输入应用名称', tpl.name + ' (Instance)');
|
||
if (name) handleFromTemplate(tpl.id, name);
|
||
}}>从模板创建</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Analytics Tab */}
|
||
{activeTab === 'analytics' && stats && (
|
||
<div>
|
||
<h3 style={{ marginBottom: '16px' }}>应用类型分布</h3>
|
||
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', marginBottom: '32px' }}>
|
||
{Object.entries(stats.by_type).map(([type, count]) => (
|
||
<div key={type} className="analytics-card">
|
||
<div className="analytics-value">{count}</div>
|
||
<div className="analytics-label">{TYPE_NAMES[type] || type}</div>
|
||
</div>
|
||
))}
|
||
{Object.keys(stats.by_type).length === 0 && (
|
||
<div style={{ color: 'var(--color-text-tertiary)' }}>暂无数据</div>
|
||
)}
|
||
</div>
|
||
|
||
<h3 style={{ marginBottom: '16px' }}>分类分布</h3>
|
||
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
|
||
{Object.entries(stats.by_category).map(([cat, count]) => (
|
||
<div key={cat} className="analytics-card">
|
||
<div className="analytics-value">{count}</div>
|
||
<div className="analytics-label">{CATEGORY_NAMES[cat] || cat}</div>
|
||
</div>
|
||
))}
|
||
{Object.keys(stats.by_category).length === 0 && (
|
||
<div style={{ color: 'var(--color-text-tertiary)' }}>暂无数据</div>
|
||
)}
|
||
</div>
|
||
|
||
<h3 style={{ marginTop: '32px', marginBottom: '16px' }}>版本统计</h3>
|
||
<div className="analytics-card" style={{ display: 'inline-block' }}>
|
||
<div className="analytics-value">{stats.total_versions}</div>
|
||
<div className="analytics-label">总版本数</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Create/Edit Modal */}
|
||
{(showCreateModal || editApp) && (
|
||
<AppFormModal
|
||
app={editApp}
|
||
onClose={() => { setShowCreateModal(false); setEditApp(null); }}
|
||
onSubmit={editApp ? handleUpdate : handleCreate}
|
||
/>
|
||
)}
|
||
|
||
{/* Template Modal */}
|
||
{showTemplateModal && (
|
||
<TemplateFormModal
|
||
onClose={() => setShowTemplateModal(false)}
|
||
onSubmit={handleCreateTemplate}
|
||
/>
|
||
)}
|
||
|
||
{/* Detail Drawer */}
|
||
{detailApp && (
|
||
<AppDetailDrawer
|
||
app={detailApp}
|
||
analytics={detailAnalytics}
|
||
onClose={() => { setDetailApp(null); setDetailAnalytics(null); }}
|
||
onAction={handleLifecycle}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// App Form Modal
|
||
// ============================================================
|
||
|
||
function AppFormModal({ app, onClose, onSubmit }: {
|
||
app: AIApp | null;
|
||
onClose: () => void;
|
||
onSubmit: (data: any) => void;
|
||
}) {
|
||
const [name, setName] = useState(app?.name || '');
|
||
const [description, setDescription] = useState(app?.description || '');
|
||
const [type, setType] = useState(app?.type || 'chatbot');
|
||
const [category, setCategory] = useState(app?.category || 'custom');
|
||
const [tags, setTags] = useState(app?.tags || '');
|
||
const [systemPrompt, setSystemPrompt] = useState(app?.config?.system_prompt || '');
|
||
const [temperature, setTemperature] = useState(app?.config?.temperature ?? 0.7);
|
||
const [maxTokens, setMaxTokens] = useState(app?.config?.max_tokens ?? 2000);
|
||
const [welcomeMessage, setWelcomeMessage] = useState(app?.config?.welcome_message || '');
|
||
|
||
const handleSubmit = () => {
|
||
if (!name.trim()) { alert('请输入应用名称'); return; }
|
||
onSubmit({
|
||
name, description, type, category, tags,
|
||
config: {
|
||
...(app?.config || {}),
|
||
system_prompt: systemPrompt,
|
||
temperature: Number(temperature),
|
||
max_tokens: Number(maxTokens),
|
||
welcome_message: welcomeMessage,
|
||
}
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="modal-overlay" onClick={onClose}>
|
||
<div className="modal-content" style={{ maxWidth: '600px' }} onClick={e => e.stopPropagation()}>
|
||
<div className="modal-header">
|
||
<h2>{app ? '编辑应用' : '创建应用'}</h2>
|
||
<button className="modal-close" onClick={onClose}>×</button>
|
||
</div>
|
||
<div className="modal-body">
|
||
<div className="form-group">
|
||
<label>应用名称 *</label>
|
||
<input type="text" value={name} onChange={e => setName(e.target.value)} placeholder="输入应用名称" />
|
||
</div>
|
||
<div className="form-row">
|
||
<div className="form-group">
|
||
<label>类型</label>
|
||
<select value={type} onChange={e => setType(e.target.value)}>
|
||
{Object.entries(TYPE_NAMES).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="form-group">
|
||
<label>分类</label>
|
||
<select value={category} onChange={e => setCategory(e.target.value)}>
|
||
{Object.entries(CATEGORY_NAMES).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div className="form-group">
|
||
<label>描述</label>
|
||
<textarea value={description} onChange={e => setDescription(e.target.value)} placeholder="应用描述" rows={2} />
|
||
</div>
|
||
<div className="form-group">
|
||
<label>标签(逗号分隔)</label>
|
||
<input type="text" value={tags} onChange={e => setTags(e.target.value)} placeholder="如:客服,电商,自动回复" />
|
||
</div>
|
||
<div className="form-section-title">AI配置</div>
|
||
<div className="form-group">
|
||
<label>系统提示词 (System Prompt)</label>
|
||
<textarea value={systemPrompt} onChange={e => setSystemPrompt(e.target.value)} placeholder="定义AI的角色和行为..." rows={4} />
|
||
</div>
|
||
<div className="form-row">
|
||
<div className="form-group">
|
||
<label>温度 (Temperature): {temperature}</label>
|
||
<input type="range" min="0" max="2" step="0.1" value={temperature} onChange={e => setTemperature(Number(e.target.value))} style={{ width: '100%' }} />
|
||
</div>
|
||
<div className="form-group">
|
||
<label>最大Token数</label>
|
||
<input type="number" value={maxTokens} onChange={e => setMaxTokens(Number(e.target.value))} min="100" max="32000" />
|
||
</div>
|
||
</div>
|
||
<div className="form-group">
|
||
<label>欢迎消息</label>
|
||
<input type="text" value={welcomeMessage} onChange={e => setWelcomeMessage(e.target.value)} placeholder="用户打开应用时显示的欢迎语" />
|
||
</div>
|
||
</div>
|
||
<div className="modal-footer">
|
||
<button className="btn-secondary" onClick={onClose}>取消</button>
|
||
<button className="btn-primary" onClick={handleSubmit}>{app ? '保存' : '创建'}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// Template Form Modal
|
||
// ============================================================
|
||
|
||
function TemplateFormModal({ onClose, onSubmit }: {
|
||
onClose: () => void;
|
||
onSubmit: (data: any) => void;
|
||
}) {
|
||
const [name, setName] = useState('');
|
||
const [description, setDescription] = useState('');
|
||
const [type, setType] = useState('chatbot');
|
||
const [category, setCategory] = useState('custom');
|
||
const [systemPrompt, setSystemPrompt] = useState('');
|
||
const [temperature, setTemperature] = useState(0.7);
|
||
const [maxTokens, setMaxTokens] = useState(2000);
|
||
|
||
const handleSubmit = () => {
|
||
if (!name.trim()) { alert('请输入模板名称'); return; }
|
||
onSubmit({
|
||
name, description, type, category,
|
||
config: {
|
||
system_prompt: systemPrompt,
|
||
temperature: Number(temperature),
|
||
max_tokens: Number(maxTokens),
|
||
skill_ids: [],
|
||
knowledge_base_ids: [],
|
||
tools: [],
|
||
welcome_message: ''
|
||
}
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="modal-overlay" onClick={onClose}>
|
||
<div className="modal-content" style={{ maxWidth: '500px' }} onClick={e => e.stopPropagation()}>
|
||
<div className="modal-header">
|
||
<h2>创建应用模板</h2>
|
||
<button className="modal-close" onClick={onClose}>×</button>
|
||
</div>
|
||
<div className="modal-body">
|
||
<div className="form-group">
|
||
<label>模板名称 *</label>
|
||
<input type="text" value={name} onChange={e => setName(e.target.value)} placeholder="如:电商客服机器人" />
|
||
</div>
|
||
<div className="form-row">
|
||
<div className="form-group">
|
||
<label>类型</label>
|
||
<select value={type} onChange={e => setType(e.target.value)}>
|
||
{Object.entries(TYPE_NAMES).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="form-group">
|
||
<label>分类</label>
|
||
<select value={category} onChange={e => setCategory(e.target.value)}>
|
||
{Object.entries(CATEGORY_NAMES).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div className="form-group">
|
||
<label>描述</label>
|
||
<textarea value={description} onChange={e => setDescription(e.target.value)} placeholder="模板描述" rows={2} />
|
||
</div>
|
||
<div className="form-group">
|
||
<label>系统提示词</label>
|
||
<textarea value={systemPrompt} onChange={e => setSystemPrompt(e.target.value)} placeholder="预置的系统提示词..." rows={4} />
|
||
</div>
|
||
<div className="form-row">
|
||
<div className="form-group">
|
||
<label>温度: {temperature}</label>
|
||
<input type="range" min="0" max="2" step="0.1" value={temperature} onChange={e => setTemperature(Number(e.target.value))} style={{ width: '100%' }} />
|
||
</div>
|
||
<div className="form-group">
|
||
<label>最大Token数</label>
|
||
<input type="number" value={maxTokens} onChange={e => setMaxTokens(Number(e.target.value))} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="modal-footer">
|
||
<button className="btn-secondary" onClick={onClose}>取消</button>
|
||
<button className="btn-primary" onClick={handleSubmit}>创建模板</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// App Detail Drawer
|
||
// ============================================================
|
||
|
||
function AppDetailDrawer({ app, analytics, onClose, onAction }: {
|
||
app: AIApp;
|
||
analytics: any;
|
||
onClose: () => void;
|
||
onAction: (id: string, action: 'publish' | 'pause' | 'archive' | 'duplicate') => void;
|
||
}) {
|
||
const a7 = app.analytics_7d as any;
|
||
const summary = analytics?.summary;
|
||
|
||
return (
|
||
<div className="modal-overlay" onClick={onClose} style={{ justifyContent: 'flex-end' }}>
|
||
<div className="modal-content" style={{ maxWidth: '560px', height: '100vh', borderRadius: 0 }} onClick={e => e.stopPropagation()}>
|
||
<div className="modal-header">
|
||
<h2>{app.name}</h2>
|
||
<button className="modal-close" onClick={onClose}>×</button>
|
||
</div>
|
||
<div className="modal-body" style={{ overflow: 'auto' }}>
|
||
{/* Status & Meta */}
|
||
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
|
||
<span className="status-badge" style={{ background: STATUS_COLORS[app.status] + '20', color: STATUS_COLORS[app.status] }}>
|
||
{STATUS_NAMES[app.status] || app.status}
|
||
</span>
|
||
<span className="badge">{TYPE_NAMES[app.type] || app.type}</span>
|
||
<span className="badge">{CATEGORY_NAMES[app.category] || app.category}</span>
|
||
<span className="badge">v{app.version}</span>
|
||
</div>
|
||
|
||
{app.description && <p style={{ color: 'var(--color-text-secondary)', marginBottom: '16px' }}>{app.description}</p>}
|
||
|
||
{/* Quick Stats */}
|
||
<div className="stats-grid" style={{ gridTemplateColumns: 'repeat(3, 1fr)', marginBottom: '24px' }}>
|
||
<div className="stat-card" style={{ padding: '12px' }}>
|
||
<div className="stat-value" style={{ fontSize: '20px' }}>{a7?.conversations || 0}</div>
|
||
<div className="stat-label">7天对话</div>
|
||
</div>
|
||
<div className="stat-card" style={{ padding: '12px' }}>
|
||
<div className="stat-value" style={{ fontSize: '20px' }}>{a7?.tokens || 0}</div>
|
||
<div className="stat-label">7天Token</div>
|
||
</div>
|
||
<div className="stat-card" style={{ padding: '12px' }}>
|
||
<div className="stat-value" style={{ fontSize: '20px' }}>{a7?.errors || 0}</div>
|
||
<div className="stat-label">7天错误</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Config */}
|
||
<h3 style={{ marginBottom: '12px' }}>应用配置</h3>
|
||
<div className="config-section">
|
||
{app.config?.system_prompt && (
|
||
<div className="config-item">
|
||
<span className="config-label">系统提示词</span>
|
||
<div className="config-value">{app.config.system_prompt}</div>
|
||
</div>
|
||
)}
|
||
<div className="config-item">
|
||
<span className="config-label">温度</span>
|
||
<div className="config-value">{app.config?.temperature ?? 'N/A'}</div>
|
||
</div>
|
||
<div className="config-item">
|
||
<span className="config-label">最大Token</span>
|
||
<div className="config-value">{app.config?.max_tokens ?? 'N/A'}</div>
|
||
</div>
|
||
{app.config?.skill_ids?.length > 0 && (
|
||
<div className="config-item">
|
||
<span className="config-label">绑定技能</span>
|
||
<div className="config-value">{app.config.skill_ids.length} 个技能</div>
|
||
</div>
|
||
)}
|
||
{app.config?.knowledge_base_ids?.length > 0 && (
|
||
<div className="config-item">
|
||
<span className="config-label">绑定知识库</span>
|
||
<div className="config-value">{app.config.knowledge_base_ids.length} 个知识库</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Versions */}
|
||
{app.versions && app.versions.length > 0 && (
|
||
<>
|
||
<h3 style={{ marginTop: '24px', marginBottom: '12px' }}>版本历史</h3>
|
||
<div className="version-list">
|
||
{app.versions.map((v: any) => (
|
||
<div key={v.id} className="version-item">
|
||
<span className="version-num">v{v.version}</span>
|
||
<span className="version-log">{v.changelog}</span>
|
||
<span className="version-date">{v.created_at ? new Date(v.created_at).toLocaleDateString('zh-CN') : '-'}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Analytics Detail */}
|
||
{summary && (
|
||
<>
|
||
<h3 style={{ marginTop: '24px', marginBottom: '12px' }}>分析数据 ({analytics.days}天)</h3>
|
||
<div className="stats-grid" style={{ gridTemplateColumns: 'repeat(2, 1fr)', marginBottom: '16px' }}>
|
||
<div className="stat-card" style={{ padding: '12px' }}>
|
||
<div className="stat-value" style={{ fontSize: '18px' }}>{summary.conversations}</div>
|
||
<div className="stat-label">总对话数</div>
|
||
</div>
|
||
<div className="stat-card" style={{ padding: '12px' }}>
|
||
<div className="stat-value" style={{ fontSize: '18px' }}>{summary.messages}</div>
|
||
<div className="stat-label">总消息数</div>
|
||
</div>
|
||
<div className="stat-card" style={{ padding: '12px' }}>
|
||
<div className="stat-value" style={{ fontSize: '18px' }}>{summary.tokens_input + summary.tokens_output}</div>
|
||
<div className="stat-label">总Token</div>
|
||
</div>
|
||
<div className="stat-card" style={{ padding: '12px' }}>
|
||
<div className="stat-value" style={{ fontSize: '18px' }}>{summary.active_users}</div>
|
||
<div className="stat-label">活跃用户</div>
|
||
</div>
|
||
<div className="stat-card" style={{ padding: '12px' }}>
|
||
<div className="stat-value" style={{ fontSize: '18px' }}>{summary.avg_response_ms}ms</div>
|
||
<div className="stat-label">平均响应</div>
|
||
</div>
|
||
<div className="stat-card" style={{ padding: '12px' }}>
|
||
<div className="stat-value" style={{ fontSize: '18px' }}>{summary.error_rate}%</div>
|
||
<div className="stat-label">错误率</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Timestamps */}
|
||
<div style={{ marginTop: '24px', fontSize: '13px', color: 'var(--color-text-tertiary)' }}>
|
||
<div>创建时间: {app.created_at ? new Date(app.created_at).toLocaleString('zh-CN') : '-'}</div>
|
||
<div>更新时间: {app.updated_at ? new Date(app.updated_at).toLocaleString('zh-CN') : '-'}</div>
|
||
{app.published_at && <div>发布时间: {new Date(app.published_at).toLocaleString('zh-CN')}</div>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="modal-footer" style={{ justifyContent: 'flex-start' }}>
|
||
{app.status === 'draft' && <button className="btn-primary" onClick={() => onAction(app.id, 'publish')}>发布</button>}
|
||
{app.status === 'published' && <button className="btn-secondary" onClick={() => onAction(app.id, 'pause')}>暂停</button>}
|
||
{app.status === 'paused' && <button className="btn-primary" onClick={() => onAction(app.id, 'publish')}>恢复发布</button>}
|
||
<button className="btn-secondary" onClick={() => onAction(app.id, 'duplicate')}>复制应用</button>
|
||
<button className="btn-secondary" onClick={() => onAction(app.id, 'archive')}>归档</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|