billing fixes
This commit is contained in:
parent
46e58bf019
commit
a544ab2c78
10 changed files with 482 additions and 257 deletions
|
|
@ -252,3 +252,28 @@ export async function fetchTransactionsAdmin(
|
|||
params: { limit }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* User summary for billing admin
|
||||
*/
|
||||
export interface MandateUserSummary {
|
||||
id: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all users for a mandate (Admin)
|
||||
* Endpoint: GET /api/billing/admin/users/{mandateId}
|
||||
*/
|
||||
export async function fetchUsersForMandateAdmin(
|
||||
request: ApiRequestFunction,
|
||||
mandateId: string
|
||||
): Promise<MandateUserSummary[]> {
|
||||
return await request({
|
||||
url: `/api/billing/admin/users/${mandateId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ export const UserSection: React.FC = () => {
|
|||
setShowMenu(false);
|
||||
};
|
||||
|
||||
const handleBilling = () => {
|
||||
navigate('/billing');
|
||||
setShowMenu(false);
|
||||
};
|
||||
|
||||
const handleLegal = () => {
|
||||
setShowLegalModal(true);
|
||||
setShowMenu(false);
|
||||
|
|
@ -72,6 +77,14 @@ export const UserSection: React.FC = () => {
|
|||
|
||||
{showMenu && (
|
||||
<div className={styles.menu}>
|
||||
<button
|
||||
className={styles.menuItem}
|
||||
onClick={handleBilling}
|
||||
>
|
||||
<span className={styles.menuIcon}>💰</span>
|
||||
Guthaben
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={styles.menuItem}
|
||||
onClick={handleSettings}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,110 @@
|
|||
.providerMultiSelect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
gap: var(--spacing-xs, 4px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.providerMultiSelect.collapsed {
|
||||
/* Collapsed state styles */
|
||||
}
|
||||
|
||||
.providerMultiSelect.expanded {
|
||||
/* Expanded state styles */
|
||||
}
|
||||
|
||||
/* Collapsible Header Button */
|
||||
.collapseHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs, 4px);
|
||||
padding: var(--spacing-xs, 4px) var(--spacing-sm, 8px);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md, 6px);
|
||||
background: var(--color-bg-input);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.collapseHeader:hover:not(:disabled) {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
.collapseHeader:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.summaryIcons {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.summaryText {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
font-size: 0.7em;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Expandable Content - opens upward */
|
||||
.expandableContent {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
margin-bottom: var(--spacing-xs, 4px);
|
||||
padding: var(--spacing-sm, 8px);
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
border-radius: var(--border-radius-md, 6px);
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.4);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.expandableContent .label {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.expandableContent .checkboxList {
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.expandableContent .checkboxItem {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.expandableContent .checkboxItem:hover {
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
.expandableContent .providerName {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.expandableContent .hint {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.expandableContent .actionButton {
|
||||
background: #3a3a3a;
|
||||
color: #e0e0e0;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.expandableContent .actionButton:hover:not(:disabled) {
|
||||
background: #4a4a4a;
|
||||
}
|
||||
|
||||
.selectActions {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
* - Lädt verfügbare Provider aus dem Billing-System
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useBilling } from '../../hooks/useBilling';
|
||||
import styles from './ProviderSelector.module.css';
|
||||
|
||||
|
|
@ -99,6 +99,7 @@ interface ProviderMultiSelectProps {
|
|||
className?: string;
|
||||
label?: string;
|
||||
showLabel?: boolean;
|
||||
defaultExpanded?: boolean;
|
||||
}
|
||||
|
||||
export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||
|
|
@ -106,9 +107,11 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
label = 'Erlaubte AI-Provider',
|
||||
label = 'AI-Provider',
|
||||
showLabel = true,
|
||||
defaultExpanded = false,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -133,8 +136,42 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
onChange([]);
|
||||
};
|
||||
|
||||
// Summary text for collapsed view
|
||||
const summaryText = useMemo(() => {
|
||||
if (selectedProviders.length === 0) {
|
||||
return 'Alle Provider';
|
||||
}
|
||||
if (selectedProviders.length === 1) {
|
||||
return PROVIDER_LABELS[selectedProviders[0]] || selectedProviders[0];
|
||||
}
|
||||
return `${selectedProviders.length} Provider`;
|
||||
}, [selectedProviders]);
|
||||
|
||||
// Summary icons for collapsed view
|
||||
const summaryIcons = useMemo(() => {
|
||||
if (selectedProviders.length === 0) {
|
||||
return '🤖';
|
||||
}
|
||||
return selectedProviders.slice(0, 3).map(p => PROVIDER_ICONS[p] || '🔌').join('');
|
||||
}, [selectedProviders]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.providerMultiSelect} ${className || ''}`}>
|
||||
<div className={`${styles.providerMultiSelect} ${className || ''} ${isExpanded ? styles.expanded : styles.collapsed}`}>
|
||||
{/* Collapsible Header */}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.collapseHeader}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className={styles.summaryIcons}>{summaryIcons}</span>
|
||||
<span className={styles.summaryText}>{summaryText}</span>
|
||||
<span className={styles.expandIcon}>{isExpanded ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
|
||||
{/* Expandable Content */}
|
||||
{isExpanded && (
|
||||
<div className={styles.expandableContent}>
|
||||
{showLabel && <label className={styles.label}>{label}</label>}
|
||||
|
||||
<div className={styles.selectActions}>
|
||||
|
|
@ -186,6 +223,8 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function useDashboardInputForm(instanceId: string) {
|
|||
const [optimisticMessage, setOptimisticMessage] = useState<WorkflowMessage | null>(null);
|
||||
const [selectedPromptId, setSelectedPromptId] = useState<string | null>(null);
|
||||
const [workflowMode, setWorkflowMode] = useState<'Dynamic' | 'Automation' | null>(null);
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>(''); // AI provider selection
|
||||
const [selectedProviders, setSelectedProviders] = useState<string[]>([]); // AI provider selection (multiselect)
|
||||
|
||||
const { checkPermission, canView } = usePermissions();
|
||||
const [playgroundUIPermission, setPlaygroundUIPermission] = useState<boolean>(false);
|
||||
|
|
@ -596,7 +596,7 @@ export function useDashboardInputForm(instanceId: string) {
|
|||
prompt: trimmedInput,
|
||||
listFileId: fileIdsToSend.length > 0 ? fileIdsToSend : undefined,
|
||||
userLanguage: 'en',
|
||||
preferredProvider: selectedProvider || undefined // AI provider selection
|
||||
preferredProviders: selectedProviders.length > 0 ? selectedProviders : undefined // AI provider selection (multiselect)
|
||||
};
|
||||
|
||||
const result = await startWorkflow(requestBody, workflowOptions);
|
||||
|
|
@ -638,7 +638,7 @@ export function useDashboardInputForm(instanceId: string) {
|
|||
setWorkflowStatusOptimistic('idle');
|
||||
}
|
||||
}
|
||||
}, [inputValue, pendingFiles, isRunning, workflowId, startingWorkflow, startWorkflow, stopWorkflow, resetWorkflow, refetchWorkflows, selectWorkflowFromContext, selectWorkflow, chatWorkflowPermission, workflowMode, selectedProvider, setWorkflowStatusOptimistic]);
|
||||
}, [inputValue, pendingFiles, isRunning, workflowId, startingWorkflow, startWorkflow, stopWorkflow, resetWorkflow, refetchWorkflows, selectWorkflowFromContext, selectWorkflow, chatWorkflowPermission, workflowMode, selectedProviders, setWorkflowStatusOptimistic]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleWorkflowCleared = () => {
|
||||
|
|
@ -823,9 +823,9 @@ export function useDashboardInputForm(instanceId: string) {
|
|||
handleFileAttach,
|
||||
handleFileUploadAndAttach,
|
||||
latestStats,
|
||||
// AI Provider selection
|
||||
selectedProvider,
|
||||
onProviderSelect: setSelectedProvider
|
||||
// AI Provider selection (multiselect)
|
||||
selectedProviders,
|
||||
onProvidersChange: setSelectedProviders
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -479,25 +479,36 @@ export function useWorkflowLifecycle(instanceId: string) {
|
|||
|
||||
// Continue polling if:
|
||||
// 1. Workflow is currently running, OR
|
||||
// 2. Workflow just completed (within last 10 seconds) - grace period to catch final messages
|
||||
// 2. Workflow just completed (within last 5 seconds) - grace period to catch final messages
|
||||
// Stop polling for failed or stopped workflows immediately
|
||||
// Use ref for statusChangedFromRunningAt to get latest value (state updates are async)
|
||||
const changedAtRef = statusChangedFromRunningAtRef.current;
|
||||
const shouldPoll = workflowStatus === 'running' ||
|
||||
(workflowStatus === 'completed' && changedAtRef !== null && Date.now() - changedAtRef < 10000);
|
||||
const gracePeriodMs = 5000; // 5 seconds grace period
|
||||
const timeSinceCompletion = changedAtRef !== null ? Date.now() - changedAtRef : Infinity;
|
||||
const isInGracePeriod = workflowStatus === 'completed' && changedAtRef !== null && timeSinceCompletion < gracePeriodMs;
|
||||
const shouldPoll = workflowStatus === 'running' || isInGracePeriod;
|
||||
|
||||
if (shouldPoll) {
|
||||
// Reset lastRenderedTimestamp for first poll (fetch all historical data)
|
||||
if (lastRenderedTimestampRef.current === null) {
|
||||
lastRenderedTimestampRef.current = null; // null means fetch all
|
||||
}
|
||||
|
||||
// Start polling
|
||||
pollingControllerRef.current.startPolling(workflowId, pollWorkflowData);
|
||||
|
||||
// If in grace period, set a timer to stop polling after grace period expires
|
||||
if (isInGracePeriod) {
|
||||
const remainingGraceTime = gracePeriodMs - timeSinceCompletion;
|
||||
const graceTimer = setTimeout(() => {
|
||||
pollingControllerRef.current.stopPolling();
|
||||
setStatusChangedFromRunningAt(null);
|
||||
statusChangedFromRunningAtRef.current = null;
|
||||
}, remainingGraceTime + 100); // Small buffer
|
||||
|
||||
return () => {
|
||||
clearTimeout(graceTimer);
|
||||
pollingControllerRef.current.stopPolling();
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Stop polling for failed, stopped, or completed (after grace period) workflows
|
||||
pollingControllerRef.current.stopPolling();
|
||||
// Clear the status change timestamp when we stop polling (only if not already null)
|
||||
// Clear the status change timestamp when we stop polling
|
||||
if (statusChangedFromRunningAt !== null) {
|
||||
setStatusChangedFromRunningAt(null);
|
||||
statusChangedFromRunningAtRef.current = null;
|
||||
|
|
@ -507,7 +518,7 @@ export function useWorkflowLifecycle(instanceId: string) {
|
|||
return () => {
|
||||
pollingControllerRef.current.stopPolling();
|
||||
};
|
||||
}, [workflowStatus, workflowId, pollWorkflowData]);
|
||||
}, [workflowStatus, workflowId, pollWorkflowData, statusChangedFromRunningAt]);
|
||||
|
||||
const handleStartWorkflow = useCallback(async (
|
||||
workflowData: StartWorkflowRequest,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
addCreditAdmin,
|
||||
fetchAccountsAdmin,
|
||||
fetchTransactionsAdmin,
|
||||
fetchUsersForMandateAdmin,
|
||||
type BillingBalance,
|
||||
type BillingTransaction,
|
||||
type BillingSettings,
|
||||
|
|
@ -25,6 +26,7 @@ import {
|
|||
type UsageReport,
|
||||
type AccountSummary,
|
||||
type CreditAddRequest,
|
||||
type MandateUserSummary,
|
||||
} from '../api/billingApi';
|
||||
|
||||
// Re-export types
|
||||
|
|
@ -36,6 +38,7 @@ export type {
|
|||
UsageReport,
|
||||
AccountSummary,
|
||||
CreditAddRequest,
|
||||
MandateUserSummary,
|
||||
};
|
||||
|
||||
export type { BillingModel, TransactionType, ReferenceType } from '../api/billingApi';
|
||||
|
|
@ -145,6 +148,7 @@ export function useBillingAdmin(mandateId?: string) {
|
|||
const [settings, setSettings] = useState<BillingSettings | null>(null);
|
||||
const [accounts, setAccounts] = useState<AccountSummary[]>([]);
|
||||
const [transactions, setTransactions] = useState<BillingTransaction[]>([]);
|
||||
const [users, setUsers] = useState<MandateUserSummary[]>([]);
|
||||
const { request, isLoading: loading, error } = useApiRequest();
|
||||
|
||||
// Fetch settings for a mandate
|
||||
|
|
@ -232,12 +236,29 @@ export function useBillingAdmin(mandateId?: string) {
|
|||
}
|
||||
}, [request, mandateId]);
|
||||
|
||||
// Fetch users for a mandate
|
||||
const loadUsers = useCallback(async (targetMandateId?: string) => {
|
||||
const mId = targetMandateId || mandateId;
|
||||
if (!mId) return [];
|
||||
|
||||
try {
|
||||
const data = await fetchUsersForMandateAdmin(request, mId);
|
||||
setUsers(Array.isArray(data) ? data : []);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Error loading users:', err);
|
||||
setUsers([]);
|
||||
return [];
|
||||
}
|
||||
}, [request, mandateId]);
|
||||
|
||||
// Load data when mandateId changes
|
||||
useEffect(() => {
|
||||
if (mandateId) {
|
||||
loadSettings();
|
||||
loadAccounts();
|
||||
loadTransactions();
|
||||
loadUsers();
|
||||
}
|
||||
}, [mandateId]);
|
||||
|
||||
|
|
@ -245,6 +266,7 @@ export function useBillingAdmin(mandateId?: string) {
|
|||
settings,
|
||||
accounts,
|
||||
transactions,
|
||||
users,
|
||||
loading,
|
||||
error,
|
||||
loadSettings,
|
||||
|
|
@ -252,11 +274,13 @@ export function useBillingAdmin(mandateId?: string) {
|
|||
addCredit,
|
||||
loadAccounts,
|
||||
loadTransactions,
|
||||
loadUsers,
|
||||
refetch: () => {
|
||||
if (mandateId) {
|
||||
loadSettings();
|
||||
loadAccounts();
|
||||
loadTransactions();
|
||||
loadUsers();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,25 +5,26 @@
|
|||
============================================================================ */
|
||||
|
||||
.billingDashboard {
|
||||
padding: var(--spacing-lg);
|
||||
padding: 1.5rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pageHeader h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #888);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
|
@ -32,23 +33,23 @@
|
|||
============================================================================ */
|
||||
|
||||
.section {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.sectionHeader .sectionTitle {
|
||||
|
|
@ -62,65 +63,65 @@
|
|||
.balanceGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.balanceCard {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--surface-color, #1e1e1e);
|
||||
border: 1px solid var(--border-color, #333);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.balanceCard:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--primary-color, #f25843);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.balanceCard.warning {
|
||||
border-color: var(--color-warning);
|
||||
background: var(--color-warning-bg, rgba(255, 193, 7, 0.1));
|
||||
border-color: #ffc107;
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
}
|
||||
|
||||
.balanceHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--spacing-md);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mandateName {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.billingModel {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-bg-secondary);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #888);
|
||||
background: var(--bg-secondary, #2a2a2a);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.balanceAmount {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.warningBadge {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-warning-text, #856404);
|
||||
background: var(--color-warning-badge-bg, rgba(255, 193, 7, 0.3));
|
||||
font-size: 0.75rem;
|
||||
color: #856404;
|
||||
background: rgba(255, 193, 7, 0.3);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
|
|
@ -129,56 +130,56 @@
|
|||
|
||||
.periodSelector {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
background: var(--color-bg-input);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color, #333);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-color, #1e1e1e);
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
border-color: var(--primary-color, #f25843);
|
||||
}
|
||||
|
||||
.statisticsChart {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--surface-color, #1e1e1e);
|
||||
border: 1px solid var(--border-color, #333);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.totalCost {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding: 1.5rem;
|
||||
background: var(--bg-secondary, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.totalLabel {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #888);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.totalAmount {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
}
|
||||
|
||||
.chartSection {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.chartSection:last-child {
|
||||
|
|
@ -186,10 +187,10 @@
|
|||
}
|
||||
|
||||
.chartSection h4 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #888);
|
||||
margin: 0 0 1rem 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
|
@ -197,34 +198,34 @@
|
|||
.barChart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.barRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.barLabel {
|
||||
width: 100px;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.barContainer {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--bg-secondary, #2a2a2a);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 100%;
|
||||
background: var(--color-primary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--primary-color, #f25843);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
min-width: 4px;
|
||||
}
|
||||
|
|
@ -232,36 +233,36 @@
|
|||
.barValue {
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #888);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.featureList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.featureRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-secondary, #2a2a2a);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.featureLabel {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.featureValue {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #888);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
|
|
@ -275,46 +276,46 @@
|
|||
|
||||
.transactionsTable th,
|
||||
.transactionsTable td {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--border-color, #333);
|
||||
}
|
||||
|
||||
.transactionsTable th {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #888);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
background: var(--color-bg-secondary);
|
||||
background: var(--bg-secondary, #2a2a2a);
|
||||
}
|
||||
|
||||
.transactionsTable td {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
}
|
||||
|
||||
.transactionType {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.transactionType.credit {
|
||||
background: var(--color-success-bg, rgba(40, 167, 69, 0.1));
|
||||
color: var(--color-success, #28a745);
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.transactionType.debit {
|
||||
background: var(--color-error-bg, rgba(220, 53, 69, 0.1));
|
||||
color: var(--color-error, #dc3545);
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.transactionType.adjustment {
|
||||
background: var(--color-info-bg, rgba(23, 162, 184, 0.1));
|
||||
color: var(--color-info, #17a2b8);
|
||||
background: rgba(23, 162, 184, 0.1);
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
|
|
@ -322,86 +323,86 @@
|
|||
============================================================================ */
|
||||
|
||||
.adminSection {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
background: var(--surface-color, #1e1e1e);
|
||||
border: 1px solid var(--border-color, #333);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.adminSection h3 {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.formRow {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.formGroup {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.formGroup label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: var(--spacing-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
background: var(--color-bg-input);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-base);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color, #333);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-color, #1e1e1e);
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
border-color: var(--primary-color, #f25843);
|
||||
}
|
||||
|
||||
.accountsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.accountCard {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-secondary, #2a2a2a);
|
||||
border: 1px solid var(--border-color, #333);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.accountCard h4 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.accountInfo span {
|
||||
color: var(--color-text-secondary);
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.accountInfo strong {
|
||||
color: var(--color-text-primary);
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
|
|
@ -409,22 +410,22 @@
|
|||
============================================================================ */
|
||||
|
||||
.button {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.buttonPrimary {
|
||||
background: var(--color-primary);
|
||||
background: var(--primary-color, #f25843);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.buttonPrimary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
background: var(--primary-dark, #d94d3a);
|
||||
}
|
||||
|
||||
.buttonPrimary:disabled {
|
||||
|
|
@ -433,13 +434,13 @@
|
|||
}
|
||||
|
||||
.buttonSecondary {
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--bg-secondary, #2a2a2a);
|
||||
color: var(--text-primary, #e0e0e0);
|
||||
border: 1px solid var(--border-color, #333);
|
||||
}
|
||||
|
||||
.buttonSecondary:hover {
|
||||
background: var(--color-bg-hover);
|
||||
background: var(--surface-color, #1e1e1e);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
|
|
@ -450,37 +451,37 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-xl);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.noData {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-lg);
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: 1.5rem;
|
||||
color: var(--text-tertiary, #666);
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
background: var(--color-error-bg, rgba(220, 53, 69, 0.1));
|
||||
color: var(--color-error, #dc3545);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius-md);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
color: #dc3545;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.successMessage {
|
||||
background: var(--color-success-bg, rgba(40, 167, 69, 0.1));
|
||||
color: var(--color-success, #28a745);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius-md);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
color: #28a745;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
|
|
@ -489,7 +490,7 @@
|
|||
|
||||
@media (max-width: 768px) {
|
||||
.billingDashboard {
|
||||
padding: var(--spacing-md);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.balanceGrid {
|
||||
|
|
@ -513,6 +514,6 @@
|
|||
.barLabel,
|
||||
.barValue {
|
||||
width: 80px;
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useBillingAdmin, type BillingSettings, type AccountSummary } from '../../hooks/useBilling';
|
||||
import { useBillingAdmin, type BillingSettings, type AccountSummary, type MandateUserSummary } from '../../hooks/useBilling';
|
||||
import { useAdminMandates } from '../../hooks/useMandates';
|
||||
import styles from './Billing.module.css';
|
||||
|
||||
|
|
@ -191,10 +191,11 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({ settings, onSave, loadi
|
|||
interface CreditAdderProps {
|
||||
settings: BillingSettings | null;
|
||||
accounts: AccountSummary[];
|
||||
users: MandateUserSummary[];
|
||||
onAddCredit: (userId: string | undefined, amount: number, description: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, onAddCredit }) => {
|
||||
const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, users, onAddCredit }) => {
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
||||
const [amount, setAmount] = useState<number>(10);
|
||||
const [description, setDescription] = useState<string>('Manuelles Aufladen');
|
||||
|
|
@ -203,6 +204,14 @@ const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, onAddCred
|
|||
|
||||
const isPrepayUser = settings?.billingModel === 'PREPAY_USER';
|
||||
|
||||
// Map accounts by userId for balance lookup
|
||||
const accountsByUserId = accounts
|
||||
.filter(acc => acc.accountType === 'USER')
|
||||
.reduce((map, acc) => {
|
||||
if (acc.userId) map[acc.userId] = acc;
|
||||
return map;
|
||||
}, {} as Record<string, AccountSummary>);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (amount <= 0) {
|
||||
|
|
@ -246,21 +255,23 @@ const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, onAddCred
|
|||
{isPrepayUser && (
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Benutzer-Konto</label>
|
||||
<label>Benutzer</label>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={selectedUserId}
|
||||
onChange={(e) => setSelectedUserId(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">-- Konto wählen --</option>
|
||||
{accounts
|
||||
.filter(acc => acc.accountType === 'USER')
|
||||
.map((acc) => (
|
||||
<option key={acc.id} value={acc.userId || ''}>
|
||||
{acc.userId} - {formatCurrency(acc.balance)}
|
||||
<option value="">-- Benutzer wählen --</option>
|
||||
{users.map((user) => {
|
||||
const account = accountsByUserId[user.id];
|
||||
const balanceInfo = account ? ` (${formatCurrency(account.balance)})` : ' (kein Konto)';
|
||||
return (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.displayName || user.email || user.id}{balanceInfo}
|
||||
</option>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -355,7 +366,7 @@ const AccountsOverview: React.FC<AccountsOverviewProps> = ({ accounts, loading }
|
|||
|
||||
export const BillingAdmin: React.FC = () => {
|
||||
const [selectedMandateId, setSelectedMandateId] = useState<string | null>(null);
|
||||
const { settings, accounts, loading, loadSettings, saveSettings, addCredit, loadAccounts } = useBillingAdmin(selectedMandateId || undefined);
|
||||
const { settings, accounts, users, loading, loadSettings, saveSettings, addCredit, loadAccounts } = useBillingAdmin(selectedMandateId || undefined);
|
||||
|
||||
const handleMandateSelect = (mandateId: string) => {
|
||||
setSelectedMandateId(mandateId || null);
|
||||
|
|
@ -397,6 +408,7 @@ export const BillingAdmin: React.FC = () => {
|
|||
<CreditAdder
|
||||
settings={settings}
|
||||
accounts={accounts}
|
||||
users={users}
|
||||
onAddCredit={handleAddCredit}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { useCurrentInstance } from '../../hooks/useCurrentInstance';
|
|||
import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus, FaMicrophone, FaSquare, FaFileAlt } from 'react-icons/fa';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { useVoiceLanguage, VoiceLanguageSelect, Messages } from '../../components/UiComponents';
|
||||
import { ProviderSelect } from '../../components/ProviderSelector';
|
||||
import { ProviderMultiSelect } from '../../components/ProviderSelector';
|
||||
import type { Message } from '../../components/UiComponents/Messages/MessagesTypes';
|
||||
import api from '../../api';
|
||||
import styles from './PlaygroundPage.module.css';
|
||||
|
|
@ -58,8 +58,8 @@ export const PlaygroundPage: React.FC = () => {
|
|||
deletingFiles,
|
||||
previewingFiles,
|
||||
downloadingFiles,
|
||||
selectedProvider,
|
||||
onProviderSelect,
|
||||
selectedProviders,
|
||||
onProvidersChange,
|
||||
} = hookData;
|
||||
|
||||
const { prompts, refetch: refetchPrompts } = usePrompts();
|
||||
|
|
@ -544,9 +544,6 @@ export const PlaygroundPage: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// Debug: Log permission status
|
||||
console.log('🔐 PlaygroundPage permission check:', { playgroundUIPermission });
|
||||
|
||||
// Permission check - also show while loading
|
||||
if (playgroundUIPermission === false) {
|
||||
return (
|
||||
|
|
@ -785,9 +782,9 @@ export const PlaygroundPage: React.FC = () => {
|
|||
>
|
||||
<FaPlus />
|
||||
</button>
|
||||
<ProviderSelect
|
||||
value={selectedProvider}
|
||||
onChange={onProviderSelect}
|
||||
<ProviderMultiSelect
|
||||
selectedProviders={selectedProviders}
|
||||
onChange={onProvidersChange}
|
||||
showLabel={false}
|
||||
/>
|
||||
<VoiceLanguageSelect
|
||||
|
|
|
|||
Loading…
Reference in a new issue