feat: version 4.1.0 -- Consumo de IA por usuario en Configuracion
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,18 @@ export const messages = {
|
||||
ai_new_project_end: 'Fin estimado',
|
||||
ai_new_project_review: 'Desmarca las actividades que no quieras. Puedes cambiar el nombre aquí o después en el proyecto.',
|
||||
ai_new_project_create: 'Crear proyecto',
|
||||
ai_usage_title: 'Consumo por usuario',
|
||||
ai_usage_month: 'Este mes',
|
||||
ai_usage_all: 'Todo',
|
||||
ai_usage_user: 'Usuario',
|
||||
ai_usage_calls: 'Consultas',
|
||||
ai_usage_input: 'Tokens entrada',
|
||||
ai_usage_output: 'Tokens salida',
|
||||
ai_usage_cost: 'Costo (USD)',
|
||||
ai_usage_last: 'Último uso',
|
||||
ai_usage_total: 'Total',
|
||||
ai_usage_hint: 'costo estimado; el monto exacto está en la consola de Anthropic.',
|
||||
ai_usage_empty: 'Todavía no hay consumo registrado.',
|
||||
nav_projects: 'Proyectos',
|
||||
nav_organization: 'Organizacion',
|
||||
nav_config: 'Configuracion',
|
||||
@@ -619,6 +631,18 @@ export const messages = {
|
||||
ai_new_project_end: 'Estimated end',
|
||||
ai_new_project_review: 'Uncheck the activities you do not want. You can change the name here or later in the project.',
|
||||
ai_new_project_create: 'Create project',
|
||||
ai_usage_title: 'Usage by user',
|
||||
ai_usage_month: 'This month',
|
||||
ai_usage_all: 'All time',
|
||||
ai_usage_user: 'User',
|
||||
ai_usage_calls: 'Requests',
|
||||
ai_usage_input: 'Input tokens',
|
||||
ai_usage_output: 'Output tokens',
|
||||
ai_usage_cost: 'Cost (USD)',
|
||||
ai_usage_last: 'Last used',
|
||||
ai_usage_total: 'Total',
|
||||
ai_usage_hint: 'estimated cost; the exact amount is in the Anthropic console.',
|
||||
ai_usage_empty: 'No usage recorded yet.',
|
||||
nav_projects: 'Projects',
|
||||
nav_organization: 'Organization',
|
||||
nav_config: 'Settings',
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useState } from 'react';
|
||||
import { useUi } from '../../context/ui-context.jsx';
|
||||
import { useAsync } from '../../hooks/useAsync.js';
|
||||
import { getAiUsage } from '../../services/settings.js';
|
||||
import { ListView } from '../../components/ListView.jsx';
|
||||
|
||||
const fmtInt = (n) => Number(n || 0).toLocaleString();
|
||||
const fmtUsd = (n) => `$${Number(n || 0).toFixed(4)}`;
|
||||
|
||||
// Consumo de IA por usuario (registrado en cada llamada). El costo es estimado con los precios
|
||||
// por millon de tokens del modelo; el monto exacto esta en la consola de Anthropic.
|
||||
export default function AiUsagePanel() {
|
||||
const { t } = useUi();
|
||||
const [period, setPeriod] = useState('month');
|
||||
const { data } = useAsync(() => getAiUsage(period), [period]);
|
||||
|
||||
const columns = [
|
||||
{ key: 'userName', label: t('ai_usage_user') },
|
||||
{ key: 'calls', label: t('ai_usage_calls'), render: (r) => fmtInt(r.calls) },
|
||||
{ key: 'inputTokens', label: t('ai_usage_input'), render: (r) => fmtInt(r.inputTokens) },
|
||||
{ key: 'outputTokens', label: t('ai_usage_output'), render: (r) => fmtInt(r.outputTokens) },
|
||||
{ key: 'costUsd', label: t('ai_usage_cost'), render: (r) => fmtUsd(r.costUsd) },
|
||||
{ key: 'lastUsed', label: t('ai_usage_last'), render: (r) => (r.lastUsed ? new Date(r.lastUsed).toLocaleString() : '—') },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="panel-card" style={{ marginTop: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
|
||||
<h4 style={{ margin: 0 }}>{t('ai_usage_title')}</h4>
|
||||
<select className="input" style={{ width: 'auto' }} value={period} onChange={(e) => setPeriod(e.target.value)}>
|
||||
<option value="month">{t('ai_usage_month')}</option>
|
||||
<option value="all">{t('ai_usage_all')}</option>
|
||||
</select>
|
||||
</div>
|
||||
{!data ? <div className="text-muted">{t('common_loading')}</div> : (
|
||||
<>
|
||||
<p className="text-muted" style={{ fontSize: '.8rem' }}>
|
||||
{t('ai_usage_total')}: <strong>{fmtInt(data.total.calls)}</strong> {t('ai_usage_calls').toLowerCase()} · {fmtInt(data.total.inputTokens)} / {fmtInt(data.total.outputTokens)} tokens · <strong>{fmtUsd(data.total.costUsd)}</strong> — {t('ai_usage_hint')}
|
||||
</p>
|
||||
{data.users.length ? (
|
||||
<ListView columns={columns} rows={data.users} getRowKey={(r) => r.userId ?? 'deleted'} />
|
||||
) : <div className="text-muted">{t('ai_usage_empty')}</div>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { ComboBox } from '../../components/ComboBox.jsx';
|
||||
import { CatalogEditor } from './CatalogEditor.jsx';
|
||||
import SmtpSettingsPanel from './SmtpSettingsPanel.jsx';
|
||||
import AiSettingsPanel from './AiSettingsPanel.jsx';
|
||||
import AiUsagePanel from './AiUsagePanel.jsx';
|
||||
import CompanySettingsPanel from './CompanySettingsPanel.jsx';
|
||||
import UpdaterPanel from './UpdaterPanel.jsx';
|
||||
import ErrorLogsPanel from './ErrorLogsPanel.jsx';
|
||||
@@ -122,6 +123,7 @@ export default function ConfiguracionPage() {
|
||||
|
||||
<Expander icon={Sparkles} title={t('config_ai')} draftForms={['ai-config']}>
|
||||
<AiSettingsPanel />
|
||||
<AiUsagePanel />
|
||||
</Expander>
|
||||
|
||||
<div id="updates-panel">
|
||||
|
||||
@@ -6,3 +6,4 @@ export const testSmtpConfig = (data) => api.post('/settings/smtp/test', data);
|
||||
export const getAiConfig = () => api.get('/settings/ai');
|
||||
export const updateAiConfig = (data) => api.put('/settings/ai', data);
|
||||
export const testAiConfig = (data) => api.post('/settings/ai/test', data);
|
||||
export const getAiUsage = (period) => api.get(`/settings/ai/usage?period=${period}`);
|
||||
|
||||
Reference in New Issue
Block a user