feat: 添加三层级模型配置管理系统原型
新增三个层级的模型配置管理功能: - 平台级模型配置(管理台):配置列表、新增/编辑、删除(默认模型不允许删除)、设为默认(仅页面状态) - 项目级模型配置(工作台):配置列表、新增/编辑、删除(允许删除默认)、设为默认(仅页面状态) - 个人模型配置(工作台):配置列表、新增/编辑、删除(允许删除默认)、设为默认(仅页面状态) - 融合式模型选择器:在聊天输入框顶部集成,按层级分组展示模型列表(平台/项目/个人) 技术实现: - 新增项目级和个人级配置数据文件 - 扩展 api.js 数据访问层,添加 consoleModels.project 和 consoleModels.user 对象 - 新增 4 个页面组件(ProjectModelConfigsPage、AddProjectModelConfigPage、UserModelConfigsPage、AddUserModelConfigPage) - 修改 2 个现有页面(ModelConfigsPage、ChatPage、ConsoleLayout) - 修改 Modal 组件支持 cancelText 为空时隐藏取消按钮 - 在 App.jsx 中添加 6 条新路由 - 新增模型选择器样式文件(融合式设计、分组展示、响应式) - 更新 README.md 项目结构 样式特点: - 融合式模型选择器与输入框风格一致 - 下拉列表按层级分组(平台/项目/个人) - 默认标记使用渐变背景色 - 选中状态高亮(浅蓝色背景 + 左侧边框 + 右侧勾选) - 响应式设计(移动端适配) 数据示例: - 项目级:3 个示例配置(不同类型和状态) - 个人级:2 个示例配置(不同类型和状态)
This commit is contained in:
10
src/App.jsx
10
src/App.jsx
@@ -23,6 +23,10 @@ import PermissionsPage from './pages/console/PermissionsPage.jsx';
|
||||
import SkillsConfigPage from './pages/console/SkillsConfigPage.jsx';
|
||||
import ConsoleReviewListPage from './pages/console/ConsoleReviewListPage.jsx';
|
||||
import ConsoleReviewDetailPage from './pages/console/ConsoleReviewDetailPage.jsx';
|
||||
import ProjectModelConfigsPage from './pages/console/ProjectModelConfigsPage.jsx';
|
||||
import AddProjectModelConfigPage from './pages/console/AddProjectModelConfigPage.jsx';
|
||||
import UserModelConfigsPage from './pages/console/UserModelConfigsPage.jsx';
|
||||
import AddUserModelConfigPage from './pages/console/AddUserModelConfigPage.jsx';
|
||||
|
||||
// Admin 子页面
|
||||
import OverviewPage from './pages/admin/OverviewPage.jsx';
|
||||
@@ -71,6 +75,12 @@ function App() {
|
||||
<Route path="project/members/:memberId/config" element={<MemberConfigPage />} />
|
||||
<Route path="project/permissions" element={<PermissionsPage />} />
|
||||
<Route path="project/skills" element={<SkillsConfigPage />} />
|
||||
<Route path="project/models" element={<ProjectModelConfigsPage />} />
|
||||
<Route path="project/models/add" element={<AddProjectModelConfigPage />} />
|
||||
<Route path="project/models/:id/edit" element={<AddProjectModelConfigPage />} />
|
||||
<Route path="user-models" element={<UserModelConfigsPage />} />
|
||||
<Route path="user-models/add" element={<AddUserModelConfigPage />} />
|
||||
<Route path="user-models/:id/edit" element={<AddUserModelConfigPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/admin" element={<AdminLayout />}>
|
||||
|
||||
@@ -27,7 +27,7 @@ function Modal({
|
||||
</div>
|
||||
{showConfirm && (
|
||||
<div className="modal-footer">
|
||||
<button className="btn" onClick={onCancel}>{cancelText}</button>
|
||||
{cancelText && <button className="btn" onClick={onCancel}>{cancelText}</button>}
|
||||
<button className="btn btn-primary" onClick={onConfirm}>{confirmText}</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { FiPlus, FiClock, FiList, FiUsers, FiBox, FiFolder, FiShield, FiSettings } from 'react-icons/fi';
|
||||
import { FiPlus, FiClock, FiList, FiUsers, FiBox, FiFolder, FiShield, FiSettings, FiCpu } from 'react-icons/fi';
|
||||
import { FiTrash2 } from 'react-icons/fi';
|
||||
import { FaPuzzlePiece } from 'react-icons/fa';
|
||||
import Layout from '../Layout.jsx';
|
||||
@@ -96,6 +96,12 @@ function ConsoleLayout() {
|
||||
active={isPathActive('/console/tasks')}
|
||||
onClick={() => navigate('/console/tasks')}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiCpu />}
|
||||
label="个人模型"
|
||||
active={isPathActive('/console/user-models')}
|
||||
onClick={() => navigate('/console/user-models')}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiList />}
|
||||
label="日志查询"
|
||||
@@ -125,6 +131,12 @@ function ConsoleLayout() {
|
||||
active={isPathActive('/console/project/skills')}
|
||||
onClick={() => navigate('/console/project/skills')}
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiCpu />}
|
||||
label="模型配置"
|
||||
active={isPathActive('/console/project/models')}
|
||||
onClick={() => navigate('/console/project/models')}
|
||||
/>
|
||||
</SidebarNavGroup>
|
||||
</div>
|
||||
<Modal
|
||||
|
||||
47
src/data/projectModelConfigs.js
Normal file
47
src/data/projectModelConfigs.js
Normal file
@@ -0,0 +1,47 @@
|
||||
export const projectModelConfigs = [
|
||||
{
|
||||
id: 'proj_001',
|
||||
name: '智算平台生产环境',
|
||||
type: 'zhisuan',
|
||||
isActive: true,
|
||||
createdAt: '2026-03-25T09:00:00',
|
||||
updatedAt: '2026-03-25T09:00:00',
|
||||
zhisuan: {
|
||||
apiUrl: 'https://zhisuan.internal.company.com/api/v1',
|
||||
appId: 'app_prod_001',
|
||||
appSecret: 'secret_prod_xyz123abc'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'proj_002',
|
||||
name: 'DeepSeek 项目专用',
|
||||
type: 'basic',
|
||||
isActive: false,
|
||||
createdAt: '2026-03-26T14:00:00',
|
||||
updatedAt: '2026-03-26T14:00:00',
|
||||
basic: {
|
||||
apiUrl: 'https://api.deepseek.com/v1',
|
||||
apiKey: 'sk-proj-deepseek789xyz',
|
||||
modelName: 'deepseek-coder',
|
||||
temperature: 0.3,
|
||||
maxTokens: 8192,
|
||||
topP: 0.95
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'proj_003',
|
||||
name: '通义千问代码助手',
|
||||
type: 'basic',
|
||||
isActive: false,
|
||||
createdAt: '2026-03-27T11:00:00',
|
||||
updatedAt: '2026-03-27T11:00:00',
|
||||
basic: {
|
||||
apiUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
apiKey: 'sk-proj-qwen456def',
|
||||
modelName: 'qwen-coder-plus',
|
||||
temperature: 0.5,
|
||||
maxTokens: 16384,
|
||||
topP: 0.8
|
||||
}
|
||||
}
|
||||
];
|
||||
34
src/data/userModelConfigs.js
Normal file
34
src/data/userModelConfigs.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export const userModelConfigs = [
|
||||
{
|
||||
id: 'user_001',
|
||||
name: '我的 GPT-4',
|
||||
type: 'basic',
|
||||
isActive: true,
|
||||
createdAt: '2026-03-26T10:00:00',
|
||||
updatedAt: '2026-03-26T10:00:00',
|
||||
basic: {
|
||||
apiUrl: 'https://api.openai.com/v1',
|
||||
apiKey: 'sk-user-abc123xyz456',
|
||||
modelName: 'gpt-4o',
|
||||
temperature: 0.7,
|
||||
maxTokens: 4096,
|
||||
topP: 0.9
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'user_002',
|
||||
name: 'Claude 备用',
|
||||
type: 'basic',
|
||||
isActive: false,
|
||||
createdAt: '2026-03-28T16:00:00',
|
||||
updatedAt: '2026-03-28T16:00:00',
|
||||
basic: {
|
||||
apiUrl: 'https://api.anthropic.com/v1',
|
||||
apiKey: 'sk-user-claude789abc',
|
||||
modelName: 'claude-3-sonnet',
|
||||
temperature: 0.8,
|
||||
maxTokens: 8192,
|
||||
topP: 0.9
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -10,6 +10,7 @@ function ModelConfigsPage() {
|
||||
const [configs, setConfigs] = useState(api.admin.modelConfigs.list());
|
||||
const [showSetActiveModal, setShowSetActiveModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showDeleteBlockedModal, setShowDeleteBlockedModal] = useState(false);
|
||||
const [selectedConfig, setSelectedConfig] = useState(null);
|
||||
|
||||
const handleSetActiveClick = (config) => {
|
||||
@@ -19,14 +20,22 @@ function ModelConfigsPage() {
|
||||
|
||||
const handleSetActiveConfirm = () => {
|
||||
if (selectedConfig) {
|
||||
api.admin.modelConfigs.setActive(selectedConfig.id);
|
||||
setConfigs([...api.admin.modelConfigs.list()]);
|
||||
const updatedConfigs = configs.map(c => ({
|
||||
...c,
|
||||
isActive: c.id === selectedConfig.id
|
||||
}));
|
||||
setConfigs(updatedConfigs);
|
||||
}
|
||||
setShowSetActiveModal(false);
|
||||
setSelectedConfig(null);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (config) => {
|
||||
if (config.isActive) {
|
||||
setSelectedConfig(config);
|
||||
setShowDeleteBlockedModal(true);
|
||||
return;
|
||||
}
|
||||
setSelectedConfig(config);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
@@ -42,7 +51,6 @@ function ModelConfigsPage() {
|
||||
|
||||
return (
|
||||
<div className="model-configs-page">
|
||||
{/* 配置列表 */}
|
||||
<div className="card">
|
||||
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div className="card-title">配置列表</div>
|
||||
@@ -86,17 +94,12 @@ function ModelConfigsPage() {
|
||||
<button
|
||||
className="text-btn text-btn-primary"
|
||||
onClick={() => navigate(`/admin/models/${config.id}/edit`)}
|
||||
disabled={config.isActive}
|
||||
title={config.isActive ? '生效中的配置不可编辑' : ''}
|
||||
style={config.isActive ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
className="text-btn text-btn-danger"
|
||||
onClick={() => handleDeleteClick(config)}
|
||||
disabled={config.isActive}
|
||||
title={config.isActive ? '生效中的配置不可删除' : ''}
|
||||
style={config.isActive ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
|
||||
>
|
||||
删除
|
||||
@@ -111,7 +114,6 @@ function ModelConfigsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设为默认确认弹窗 */}
|
||||
<Modal
|
||||
visible={showSetActiveModal}
|
||||
title="确认切换默认配置"
|
||||
@@ -127,7 +129,6 @@ function ModelConfigsPage() {
|
||||
<p>切换后,原生效配置将变为备用状态。</p>
|
||||
</Modal>
|
||||
|
||||
{/* 删除确认弹窗 */}
|
||||
<Modal
|
||||
visible={showDeleteModal}
|
||||
title="确认删除"
|
||||
@@ -142,6 +143,24 @@ function ModelConfigsPage() {
|
||||
<p>确定要删除配置"{selectedConfig?.name}"吗?</p>
|
||||
<p>此操作不可恢复。</p>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
visible={showDeleteBlockedModal}
|
||||
title="无法删除"
|
||||
onConfirm={() => {
|
||||
setShowDeleteBlockedModal(false);
|
||||
setSelectedConfig(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowDeleteBlockedModal(false);
|
||||
setSelectedConfig(null);
|
||||
}}
|
||||
confirmText="我知道了"
|
||||
cancelText=""
|
||||
>
|
||||
<p>平台默认模型不允许删除。</p>
|
||||
<p>请先将另一个配置设为默认,然后再删除此配置。</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
203
src/pages/console/AddProjectModelConfigPage.jsx
Normal file
203
src/pages/console/AddProjectModelConfigPage.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { FiEye, FiEyeOff } from 'react-icons/fi';
|
||||
import { api } from '../../services/api.js';
|
||||
import { MODEL_CONFIG_TYPES, getConfigFields, getConfigTypeList } from '../../data/configTypes.js';
|
||||
|
||||
function AddProjectModelConfigPage() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const editData = id ? api.consoleModels.project.getById(id) : null;
|
||||
const isEdit = !!editData;
|
||||
|
||||
const [configName, setConfigName] = useState('');
|
||||
const [configType, setConfigType] = useState('basic');
|
||||
const [fieldValues, setFieldValues] = useState({});
|
||||
const [showPasswords, setShowPasswords] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (editData) {
|
||||
setConfigName(editData.name || '');
|
||||
setConfigType(editData.type || 'basic');
|
||||
const typeData = editData[editData.type] || {};
|
||||
const initialValues = {};
|
||||
const fields = getConfigFields(editData.type);
|
||||
fields.forEach(field => {
|
||||
initialValues[field.key] = typeData[field.key] ?? field.default ?? '';
|
||||
});
|
||||
setFieldValues(initialValues);
|
||||
} else {
|
||||
const fields = getConfigFields('basic');
|
||||
const initialValues = {};
|
||||
fields.forEach(field => {
|
||||
initialValues[field.key] = field.default ?? '';
|
||||
});
|
||||
setFieldValues(initialValues);
|
||||
}
|
||||
}, [editData]);
|
||||
|
||||
const handleTypeChange = (newType) => {
|
||||
if (isEdit) return;
|
||||
setConfigType(newType);
|
||||
const fields = getConfigFields(newType);
|
||||
const newValues = {};
|
||||
fields.forEach(field => {
|
||||
newValues[field.key] = field.default ?? '';
|
||||
});
|
||||
setFieldValues(newValues);
|
||||
};
|
||||
|
||||
const handleFieldChange = (key, value) => {
|
||||
setFieldValues(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const togglePasswordVisibility = (key) => {
|
||||
setShowPasswords(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const typeData = {};
|
||||
const fields = getConfigFields(configType);
|
||||
fields.forEach(field => {
|
||||
let value = fieldValues[field.key];
|
||||
if (field.type === 'number' && value !== '') {
|
||||
value = Number(value);
|
||||
}
|
||||
typeData[field.key] = value;
|
||||
});
|
||||
|
||||
const configData = {
|
||||
name: configName.trim(),
|
||||
type: configType,
|
||||
[configType]: typeData
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
api.consoleModels.project.update(editData.id, configData);
|
||||
} else {
|
||||
api.consoleModels.project.create(configData);
|
||||
}
|
||||
|
||||
navigate('/console/project/models');
|
||||
};
|
||||
|
||||
const currentFields = getConfigFields(configType);
|
||||
const configTypeList = getConfigTypeList();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-back-btn" onClick={() => navigate('/console/project/models')}>
|
||||
<span>←</span>
|
||||
<span>返回配置列表</span>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="card-title">{isEdit ? '编辑配置' : '新增配置'}</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="form-group">
|
||||
<label className="form-label required">配置名称</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="请输入配置名称"
|
||||
value={configName}
|
||||
onChange={(e) => setConfigName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label required">配置类型</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={configType}
|
||||
onChange={(e) => handleTypeChange(e.target.value)}
|
||||
disabled={isEdit}
|
||||
>
|
||||
{configTypeList.map(type => (
|
||||
<option key={type.key} value={type.key}>
|
||||
{type.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isEdit && (
|
||||
<div style={{ fontSize: '12px', color: '#6B7280', marginTop: '4px' }}>
|
||||
配置类型不可修改
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '24px', marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '14px', fontWeight: 600, color: '#111827', marginBottom: '16px' }}>
|
||||
{MODEL_CONFIG_TYPES[configType]?.label} 配置
|
||||
</div>
|
||||
|
||||
{currentFields.map(field => (
|
||||
<div key={field.key} className="form-group">
|
||||
<label className={`form-label ${field.required ? 'required' : ''}`}>
|
||||
{field.label}
|
||||
</label>
|
||||
|
||||
{field.type === 'password' ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
type={showPasswords[field.key] ? 'text' : 'password'}
|
||||
className="form-control"
|
||||
style={{ paddingRight: '40px' }}
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePasswordVisibility(field.key)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: '6px',
|
||||
cursor: 'pointer',
|
||||
color: '#6B7280'
|
||||
}}
|
||||
>
|
||||
{showPasswords[field.key] ? <FiEyeOff size={16} /> : <FiEye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
) : field.type === 'number' ? (
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type={field.type === 'url' ? 'url' : 'text'}
|
||||
className="form-control"
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
|
||||
<button className="btn" onClick={() => navigate('/console/project/models')}>取消</button>
|
||||
<button className="btn btn-primary" onClick={handleSave}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddProjectModelConfigPage;
|
||||
203
src/pages/console/AddUserModelConfigPage.jsx
Normal file
203
src/pages/console/AddUserModelConfigPage.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { FiEye, FiEyeOff } from 'react-icons/fi';
|
||||
import { api } from '../../services/api.js';
|
||||
import { MODEL_CONFIG_TYPES, getConfigFields, getConfigTypeList } from '../../data/configTypes.js';
|
||||
|
||||
function AddUserModelConfigPage() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const editData = id ? api.consoleModels.user.getById(id) : null;
|
||||
const isEdit = !!editData;
|
||||
|
||||
const [configName, setConfigName] = useState('');
|
||||
const [configType, setConfigType] = useState('basic');
|
||||
const [fieldValues, setFieldValues] = useState({});
|
||||
const [showPasswords, setShowPasswords] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (editData) {
|
||||
setConfigName(editData.name || '');
|
||||
setConfigType(editData.type || 'basic');
|
||||
const typeData = editData[editData.type] || {};
|
||||
const initialValues = {};
|
||||
const fields = getConfigFields(editData.type);
|
||||
fields.forEach(field => {
|
||||
initialValues[field.key] = typeData[field.key] ?? field.default ?? '';
|
||||
});
|
||||
setFieldValues(initialValues);
|
||||
} else {
|
||||
const fields = getConfigFields('basic');
|
||||
const initialValues = {};
|
||||
fields.forEach(field => {
|
||||
initialValues[field.key] = field.default ?? '';
|
||||
});
|
||||
setFieldValues(initialValues);
|
||||
}
|
||||
}, [editData]);
|
||||
|
||||
const handleTypeChange = (newType) => {
|
||||
if (isEdit) return;
|
||||
setConfigType(newType);
|
||||
const fields = getConfigFields(newType);
|
||||
const newValues = {};
|
||||
fields.forEach(field => {
|
||||
newValues[field.key] = field.default ?? '';
|
||||
});
|
||||
setFieldValues(newValues);
|
||||
};
|
||||
|
||||
const handleFieldChange = (key, value) => {
|
||||
setFieldValues(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const togglePasswordVisibility = (key) => {
|
||||
setShowPasswords(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const typeData = {};
|
||||
const fields = getConfigFields(configType);
|
||||
fields.forEach(field => {
|
||||
let value = fieldValues[field.key];
|
||||
if (field.type === 'number' && value !== '') {
|
||||
value = Number(value);
|
||||
}
|
||||
typeData[field.key] = value;
|
||||
});
|
||||
|
||||
const configData = {
|
||||
name: configName.trim(),
|
||||
type: configType,
|
||||
[configType]: typeData
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
api.consoleModels.user.update(editData.id, configData);
|
||||
} else {
|
||||
api.consoleModels.user.create(configData);
|
||||
}
|
||||
|
||||
navigate('/console/user-models');
|
||||
};
|
||||
|
||||
const currentFields = getConfigFields(configType);
|
||||
const configTypeList = getConfigTypeList();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-back-btn" onClick={() => navigate('/console/user-models')}>
|
||||
<span>←</span>
|
||||
<span>返回配置列表</span>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="card-title">{isEdit ? '编辑配置' : '新增配置'}</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="form-group">
|
||||
<label className="form-label required">配置名称</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="请输入配置名称"
|
||||
value={configName}
|
||||
onChange={(e) => setConfigName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label required">配置类型</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={configType}
|
||||
onChange={(e) => handleTypeChange(e.target.value)}
|
||||
disabled={isEdit}
|
||||
>
|
||||
{configTypeList.map(type => (
|
||||
<option key={type.key} value={type.key}>
|
||||
{type.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isEdit && (
|
||||
<div style={{ fontSize: '12px', color: '#6B7280', marginTop: '4px' }}>
|
||||
配置类型不可修改
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '24px', marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '14px', fontWeight: 600, color: '#111827', marginBottom: '16px' }}>
|
||||
{MODEL_CONFIG_TYPES[configType]?.label} 配置
|
||||
</div>
|
||||
|
||||
{currentFields.map(field => (
|
||||
<div key={field.key} className="form-group">
|
||||
<label className={`form-label ${field.required ? 'required' : ''}`}>
|
||||
{field.label}
|
||||
</label>
|
||||
|
||||
{field.type === 'password' ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
type={showPasswords[field.key] ? 'text' : 'password'}
|
||||
className="form-control"
|
||||
style={{ paddingRight: '40px' }}
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePasswordVisibility(field.key)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: '6px',
|
||||
cursor: 'pointer',
|
||||
color: '#6B7280'
|
||||
}}
|
||||
>
|
||||
{showPasswords[field.key] ? <FiEyeOff size={16} /> : <FiEye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
) : field.type === 'number' ? (
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type={field.type === 'url' ? 'url' : 'text'}
|
||||
className="form-control"
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
|
||||
<button className="btn" onClick={() => navigate('/console/user-models')}>取消</button>
|
||||
<button className="btn btn-primary" onClick={handleSave}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddUserModelConfigPage;
|
||||
@@ -1,7 +1,99 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { getChatScenes } from '../../data/conversations.js';
|
||||
import { FiPaperclip, FiCode, FiSend } from 'react-icons/fi';
|
||||
import { FiPaperclip, FiCode, FiSend, FiChevronDown } from 'react-icons/fi';
|
||||
import { api } from '../../services/api.js';
|
||||
|
||||
function ModelSelector({ selectedModel, onSelectModel }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
const platformModels = api.admin.modelConfigs.list().map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
level: 'platform',
|
||||
isDefault: c.isActive,
|
||||
}));
|
||||
|
||||
const projectModels = api.consoleModels.project.list().map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
level: 'project',
|
||||
isDefault: c.isActive,
|
||||
}));
|
||||
|
||||
const userModels = api.consoleModels.user.list().map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
level: 'user',
|
||||
isDefault: c.isActive,
|
||||
}));
|
||||
|
||||
const groups = [
|
||||
{ key: 'platform', title: '平台模型', models: platformModels },
|
||||
{ key: 'project', title: '项目模型', models: projectModels },
|
||||
{ key: 'user', title: '个人模型', models: userModels },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
if (ref.current && !ref.current.contains(e.target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleSelect = (model) => {
|
||||
onSelectModel(model);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`model-selector ${open ? 'model-selector--open' : ''}`} ref={ref}>
|
||||
<div
|
||||
className="model-selector__trigger"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<div className="model-selector__content">
|
||||
<span className="model-selector__icon">
|
||||
<FiCode size={12} />
|
||||
</span>
|
||||
<span className="model-selector__name">{selectedModel?.name || '选择模型'}</span>
|
||||
{selectedModel?.isDefault && (
|
||||
<span className="model-selector__tag">默认</span>
|
||||
)}
|
||||
</div>
|
||||
<span className={`model-selector__arrow ${open ? 'model-selector__arrow--open' : ''}`}>
|
||||
<FiChevronDown size={14} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<div className="model-selector__dropdown">
|
||||
{groups.map(group => (
|
||||
<div key={group.key} className="model-selector__group">
|
||||
<div className="model-selector__group-title">{group.title}</div>
|
||||
{group.models.map(model => (
|
||||
<div
|
||||
key={model.id}
|
||||
className={`model-selector__item ${selectedModel?.id === model.id ? 'model-selector__item--selected' : ''}`}
|
||||
onClick={() => handleSelect(model)}
|
||||
>
|
||||
<span className="model-selector__item-text">{model.name}</span>
|
||||
{model.isDefault && (
|
||||
<span className="model-selector__item-tag">默认</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatPage() {
|
||||
const { scene } = useParams();
|
||||
@@ -10,6 +102,17 @@ function ChatPage() {
|
||||
const html = chatScenes[currentScene] || '';
|
||||
const chatMessagesRef = useRef(null);
|
||||
|
||||
const defaultPlatformModel = api.admin.modelConfigs.list().find(c => c.isActive);
|
||||
const defaultProjectModel = api.consoleModels.project.list().find(c => c.isActive);
|
||||
const defaultUserModel = api.consoleModels.user.list().find(c => c.isActive);
|
||||
const initialModel = defaultUserModel || defaultProjectModel || defaultPlatformModel;
|
||||
|
||||
const [selectedModel, setSelectedModel] = useState(() => {
|
||||
if (!initialModel) return null;
|
||||
const level = defaultUserModel ? 'user' : (defaultProjectModel ? 'project' : 'platform');
|
||||
return { id: initialModel.id, name: initialModel.name, level, isDefault: true };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatMessagesRef.current) return;
|
||||
|
||||
@@ -30,7 +133,7 @@ function ChatPage() {
|
||||
el.removeEventListener('click', handleClick);
|
||||
});
|
||||
};
|
||||
}, [scene, html]); // 依赖场景和html内容
|
||||
}, [scene, html]);
|
||||
|
||||
return (
|
||||
<div className="chat-layout">
|
||||
@@ -41,6 +144,10 @@ function ChatPage() {
|
||||
<div className="chat-input-wrapper">
|
||||
<div className="chat-input-container">
|
||||
<div className="chat-input-box">
|
||||
<ModelSelector
|
||||
selectedModel={selectedModel}
|
||||
onSelectModel={setSelectedModel}
|
||||
/>
|
||||
<div className="chat-input-main">
|
||||
<textarea
|
||||
className="chat-input"
|
||||
@@ -69,4 +176,4 @@ function ChatPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatPage;
|
||||
export default ChatPage;
|
||||
|
||||
143
src/pages/console/ProjectModelConfigsPage.jsx
Normal file
143
src/pages/console/ProjectModelConfigsPage.jsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FiPlus } from 'react-icons/fi';
|
||||
import { api } from '../../services/api.js';
|
||||
import { MODEL_CONFIG_TYPES } from '../../data/configTypes.js';
|
||||
import Modal from '../../components/common/Modal.jsx';
|
||||
|
||||
function ProjectModelConfigsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [configs, setConfigs] = useState(api.consoleModels.project.list());
|
||||
const [showSetActiveModal, setShowSetActiveModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [selectedConfig, setSelectedConfig] = useState(null);
|
||||
|
||||
const handleSetActiveClick = (config) => {
|
||||
setSelectedConfig(config);
|
||||
setShowSetActiveModal(true);
|
||||
};
|
||||
|
||||
const handleSetActiveConfirm = () => {
|
||||
if (selectedConfig) {
|
||||
const updatedConfigs = configs.map(c => ({
|
||||
...c,
|
||||
isActive: c.id === selectedConfig.id
|
||||
}));
|
||||
setConfigs(updatedConfigs);
|
||||
}
|
||||
setShowSetActiveModal(false);
|
||||
setSelectedConfig(null);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (config) => {
|
||||
setSelectedConfig(config);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (selectedConfig) {
|
||||
api.consoleModels.project.delete(selectedConfig.id);
|
||||
setConfigs([...api.consoleModels.project.list()]);
|
||||
}
|
||||
setShowDeleteModal(false);
|
||||
setSelectedConfig(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="model-configs-page">
|
||||
<div className="card">
|
||||
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div className="card-title">项目模型配置</div>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => navigate('/console/project/models/add')}>
|
||||
<FiPlus /> 新增配置
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="table-wrapper">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>配置名称</th>
|
||||
<th>配置类型</th>
|
||||
<th>状态</th>
|
||||
<th className="col-actions">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{configs.map(config => (
|
||||
<tr key={config.id} className={config.isActive ? 'active-row' : ''}>
|
||||
<td><strong>{config.name}</strong></td>
|
||||
<td>{MODEL_CONFIG_TYPES[config.type]?.label || config.type}</td>
|
||||
<td>
|
||||
{config.isActive ? (
|
||||
<span className="status status-running">生效中</span>
|
||||
) : (
|
||||
<span className="status status-stopped">未生效</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="col-actions">
|
||||
<div className="table-actions">
|
||||
{!config.isActive && (
|
||||
<button
|
||||
className="text-btn text-btn-primary"
|
||||
onClick={() => handleSetActiveClick(config)}
|
||||
>
|
||||
设为默认
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="text-btn text-btn-primary"
|
||||
onClick={() => navigate(`/console/project/models/${config.id}/edit`)}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
className="text-btn text-btn-danger"
|
||||
onClick={() => handleDeleteClick(config)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
visible={showSetActiveModal}
|
||||
title="确认切换默认配置"
|
||||
onConfirm={handleSetActiveConfirm}
|
||||
onCancel={() => {
|
||||
setShowSetActiveModal(false);
|
||||
setSelectedConfig(null);
|
||||
}}
|
||||
confirmText="确认切换"
|
||||
cancelText="取消"
|
||||
>
|
||||
<p>确定将"{selectedConfig?.name}"设为项目默认模型配置吗?</p>
|
||||
<p>切换后,原生效配置将变为备用状态。</p>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
visible={showDeleteModal}
|
||||
title="确认删除"
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={() => {
|
||||
setShowDeleteModal(false);
|
||||
setSelectedConfig(null);
|
||||
}}
|
||||
confirmText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<p>确定要删除配置"{selectedConfig?.name}"吗?</p>
|
||||
<p>此操作不可恢复。</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectModelConfigsPage;
|
||||
143
src/pages/console/UserModelConfigsPage.jsx
Normal file
143
src/pages/console/UserModelConfigsPage.jsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FiPlus } from 'react-icons/fi';
|
||||
import { api } from '../../services/api.js';
|
||||
import { MODEL_CONFIG_TYPES } from '../../data/configTypes.js';
|
||||
import Modal from '../../components/common/Modal.jsx';
|
||||
|
||||
function UserModelConfigsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [configs, setConfigs] = useState(api.consoleModels.user.list());
|
||||
const [showSetActiveModal, setShowSetActiveModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [selectedConfig, setSelectedConfig] = useState(null);
|
||||
|
||||
const handleSetActiveClick = (config) => {
|
||||
setSelectedConfig(config);
|
||||
setShowSetActiveModal(true);
|
||||
};
|
||||
|
||||
const handleSetActiveConfirm = () => {
|
||||
if (selectedConfig) {
|
||||
const updatedConfigs = configs.map(c => ({
|
||||
...c,
|
||||
isActive: c.id === selectedConfig.id
|
||||
}));
|
||||
setConfigs(updatedConfigs);
|
||||
}
|
||||
setShowSetActiveModal(false);
|
||||
setSelectedConfig(null);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (config) => {
|
||||
setSelectedConfig(config);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (selectedConfig) {
|
||||
api.consoleModels.user.delete(selectedConfig.id);
|
||||
setConfigs([...api.consoleModels.user.list()]);
|
||||
}
|
||||
setShowDeleteModal(false);
|
||||
setSelectedConfig(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="model-configs-page">
|
||||
<div className="card">
|
||||
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div className="card-title">个人模型配置</div>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => navigate('/console/user-models/add')}>
|
||||
<FiPlus /> 新增配置
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="table-wrapper">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>配置名称</th>
|
||||
<th>配置类型</th>
|
||||
<th>状态</th>
|
||||
<th className="col-actions">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{configs.map(config => (
|
||||
<tr key={config.id} className={config.isActive ? 'active-row' : ''}>
|
||||
<td><strong>{config.name}</strong></td>
|
||||
<td>{MODEL_CONFIG_TYPES[config.type]?.label || config.type}</td>
|
||||
<td>
|
||||
{config.isActive ? (
|
||||
<span className="status status-running">生效中</span>
|
||||
) : (
|
||||
<span className="status status-stopped">未生效</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="col-actions">
|
||||
<div className="table-actions">
|
||||
{!config.isActive && (
|
||||
<button
|
||||
className="text-btn text-btn-primary"
|
||||
onClick={() => handleSetActiveClick(config)}
|
||||
>
|
||||
设为默认
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="text-btn text-btn-primary"
|
||||
onClick={() => navigate(`/console/user-models/${config.id}/edit`)}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
className="text-btn text-btn-danger"
|
||||
onClick={() => handleDeleteClick(config)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
visible={showSetActiveModal}
|
||||
title="确认切换默认配置"
|
||||
onConfirm={handleSetActiveConfirm}
|
||||
onCancel={() => {
|
||||
setShowSetActiveModal(false);
|
||||
setSelectedConfig(null);
|
||||
}}
|
||||
confirmText="确认切换"
|
||||
cancelText="取消"
|
||||
>
|
||||
<p>确定将"{selectedConfig?.name}"设为个人默认模型配置吗?</p>
|
||||
<p>切换后,原生效配置将变为备用状态。</p>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
visible={showDeleteModal}
|
||||
title="确认删除"
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={() => {
|
||||
setShowDeleteModal(false);
|
||||
setSelectedConfig(null);
|
||||
}}
|
||||
confirmText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<p>确定要删除配置"{selectedConfig?.name}"吗?</p>
|
||||
<p>此操作不可恢复。</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserModelConfigsPage;
|
||||
@@ -11,6 +11,8 @@ import { mySkills, skillCategories, devDocs, developerOverview } from '../data/d
|
||||
import { projectMembers } from '../data/members.js';
|
||||
import { scheduledTasks } from '../data/tasks.js';
|
||||
import { adminDepartments, adminUsers, adminProjects, adminOverview, adminLogs, modelConfigs } from '../data/adminData.js';
|
||||
import { projectModelConfigs } from '../data/projectModelConfigs.js';
|
||||
import { userModelConfigs } from '../data/userModelConfigs.js';
|
||||
|
||||
/**
|
||||
* 用户相关 API
|
||||
@@ -377,8 +379,81 @@ export const adminApi = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一 API 导出对象
|
||||
* 控制台模型配置相关 API(项目级 + 个人级)
|
||||
*/
|
||||
export const consoleModelConfigsApi = {
|
||||
project: {
|
||||
list: () => projectModelConfigs,
|
||||
getById: (id) => projectModelConfigs.find(c => c.id === id),
|
||||
create: (data) => {
|
||||
const newConfig = {
|
||||
...data,
|
||||
id: `proj_${String(projectModelConfigs.length + 1).padStart(3, '0')}`,
|
||||
isActive: projectModelConfigs.length === 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
projectModelConfigs.push(newConfig);
|
||||
return newConfig;
|
||||
},
|
||||
update: (id, data) => {
|
||||
const index = projectModelConfigs.findIndex(c => c.id === id);
|
||||
if (index === -1) return undefined;
|
||||
projectModelConfigs[index] = { ...projectModelConfigs[index], ...data, updatedAt: new Date().toISOString() };
|
||||
return projectModelConfigs[index];
|
||||
},
|
||||
delete: (id) => {
|
||||
const index = projectModelConfigs.findIndex(c => c.id === id);
|
||||
if (index === -1) return false;
|
||||
projectModelConfigs.splice(index, 1);
|
||||
return true;
|
||||
},
|
||||
setActive: (id) => {
|
||||
const target = projectModelConfigs.find(c => c.id === id);
|
||||
if (!target) return undefined;
|
||||
projectModelConfigs.forEach(c => { c.isActive = false; });
|
||||
target.isActive = true;
|
||||
target.updatedAt = new Date().toISOString();
|
||||
return target;
|
||||
},
|
||||
},
|
||||
|
||||
user: {
|
||||
list: () => userModelConfigs,
|
||||
getById: (id) => userModelConfigs.find(c => c.id === id),
|
||||
create: (data) => {
|
||||
const newConfig = {
|
||||
...data,
|
||||
id: `user_${String(userModelConfigs.length + 1).padStart(3, '0')}`,
|
||||
isActive: userModelConfigs.length === 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
userModelConfigs.push(newConfig);
|
||||
return newConfig;
|
||||
},
|
||||
update: (id, data) => {
|
||||
const index = userModelConfigs.findIndex(c => c.id === id);
|
||||
if (index === -1) return undefined;
|
||||
userModelConfigs[index] = { ...userModelConfigs[index], ...data, updatedAt: new Date().toISOString() };
|
||||
return userModelConfigs[index];
|
||||
},
|
||||
delete: (id) => {
|
||||
const index = userModelConfigs.findIndex(c => c.id === id);
|
||||
if (index === -1) return false;
|
||||
userModelConfigs.splice(index, 1);
|
||||
return true;
|
||||
},
|
||||
setActive: (id) => {
|
||||
const target = userModelConfigs.find(c => c.id === id);
|
||||
if (!target) return undefined;
|
||||
userModelConfigs.forEach(c => { c.isActive = false; });
|
||||
target.isActive = true;
|
||||
target.updatedAt = new Date().toISOString();
|
||||
return target;
|
||||
},
|
||||
},
|
||||
};
|
||||
export const api = {
|
||||
user,
|
||||
skills: skillsApi,
|
||||
@@ -388,6 +463,7 @@ export const api = {
|
||||
members: membersApi,
|
||||
tasks: tasksApi,
|
||||
admin: adminApi,
|
||||
consoleModels: consoleModelConfigsApi,
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
@use 'pages/admin' as *;
|
||||
@use 'pages/developer' as *;
|
||||
@use 'pages/home' as *;
|
||||
@use 'pages/model-selector' as *;
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
@forward 'admin';
|
||||
@forward 'developer';
|
||||
@forward 'home';
|
||||
@forward 'model-selector';
|
||||
|
||||
219
src/styles/pages/model-selector/_index.scss
Normal file
219
src/styles/pages/model-selector/_index.scss
Normal file
@@ -0,0 +1,219 @@
|
||||
@use '../../tokens' as *;
|
||||
|
||||
.model-selector {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.model-selector--open {
|
||||
.model-selector__arrow--open {
|
||||
transform: rotate(180deg);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.model-selector__trigger {
|
||||
background: var(--color-bg-3);
|
||||
}
|
||||
}
|
||||
|
||||
.model-selector__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-2;
|
||||
padding: 6px $spacing-3;
|
||||
background: var(--color-bg-2);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition);
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-3);
|
||||
}
|
||||
}
|
||||
|
||||
.model-selector__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-2;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.model-selector__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, #8B5CF6 100%);
|
||||
border-radius: $radius-sm;
|
||||
color: #FFFFFF;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.model-selector__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
.model-selector__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 6px;
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, #60A5FA 100%);
|
||||
color: #FFFFFF;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
border-radius: 999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.model-selector__arrow {
|
||||
color: var(--color-text-3);
|
||||
transition: transform var(--transition);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.model-selector__dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-bg-1);
|
||||
border: 1px solid var(--color-primary);
|
||||
border-top: none;
|
||||
border-radius: 0 0 $radius-lg $radius-lg;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12), 0 2px 8px rgba(15, 23, 42, 0.06);
|
||||
overflow: hidden;
|
||||
animation: dropdownExpand 0.2s ease-out;
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border-3);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-text-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdownExpand {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.model-selector__group {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
}
|
||||
}
|
||||
|
||||
.model-selector__group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px $spacing-3;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-3);
|
||||
background: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.model-selector__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-2;
|
||||
padding: 8px $spacing-3;
|
||||
padding-right: 32px;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: var(--color-primary-light);
|
||||
|
||||
&::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
right: $spacing-3;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-selector__item-text {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-1);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
.model-selector__item-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 5px;
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, #60A5FA 100%);
|
||||
color: #FFFFFF;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
border-radius: 999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.model-selector__trigger {
|
||||
padding: 4px $spacing-2;
|
||||
}
|
||||
|
||||
.model-selector__name {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-selector__dropdown {
|
||||
max-height: 280px;
|
||||
}
|
||||
|
||||
.model-selector__item {
|
||||
padding: 6px $spacing-2;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.model-selector__item-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user