Commit 429f38d0 authored by shaw's avatar shaw
Browse files

Merge PR #37: Add Gemini OAuth and Messages Compat Support

parents 2d89f366 2714be99
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
* Handles redeem code generation and management for administrators * Handles redeem code generation and management for administrators
*/ */
import { apiClient } from '../client'; import { apiClient } from '../client'
import type { import type {
RedeemCode, RedeemCode,
GenerateRedeemCodesRequest, GenerateRedeemCodesRequest,
RedeemCodeType, RedeemCodeType,
PaginatedResponse, PaginatedResponse
} from '@/types'; } from '@/types'
/** /**
* List all redeem codes with pagination * List all redeem codes with pagination
...@@ -22,19 +22,19 @@ export async function list( ...@@ -22,19 +22,19 @@ export async function list(
page: number = 1, page: number = 1,
pageSize: number = 20, pageSize: number = 20,
filters?: { filters?: {
type?: RedeemCodeType; type?: RedeemCodeType
status?: 'active' | 'used' | 'expired' | 'unused'; status?: 'active' | 'used' | 'expired' | 'unused'
search?: string; search?: string
} }
): Promise<PaginatedResponse<RedeemCode>> { ): Promise<PaginatedResponse<RedeemCode>> {
const { data } = await apiClient.get<PaginatedResponse<RedeemCode>>('/admin/redeem-codes', { const { data } = await apiClient.get<PaginatedResponse<RedeemCode>>('/admin/redeem-codes', {
params: { params: {
page, page,
page_size: pageSize, page_size: pageSize,
...filters, ...filters
}, }
}); })
return data; return data
} }
/** /**
...@@ -43,8 +43,8 @@ export async function list( ...@@ -43,8 +43,8 @@ export async function list(
* @returns Redeem code details * @returns Redeem code details
*/ */
export async function getById(id: number): Promise<RedeemCode> { export async function getById(id: number): Promise<RedeemCode> {
const { data } = await apiClient.get<RedeemCode>(`/admin/redeem-codes/${id}`); const { data } = await apiClient.get<RedeemCode>(`/admin/redeem-codes/${id}`)
return data; return data
} }
/** /**
...@@ -66,19 +66,19 @@ export async function generate( ...@@ -66,19 +66,19 @@ export async function generate(
const payload: GenerateRedeemCodesRequest = { const payload: GenerateRedeemCodesRequest = {
count, count,
type, type,
value, value
}; }
// 订阅类型专用字段 // 订阅类型专用字段
if (type === 'subscription') { if (type === 'subscription') {
payload.group_id = groupId; payload.group_id = groupId
if (validityDays && validityDays > 0) { if (validityDays && validityDays > 0) {
payload.validity_days = validityDays; payload.validity_days = validityDays
} }
} }
const { data } = await apiClient.post<RedeemCode[]>('/admin/redeem-codes/generate', payload); const { data } = await apiClient.post<RedeemCode[]>('/admin/redeem-codes/generate', payload)
return data; return data
} }
/** /**
...@@ -87,8 +87,8 @@ export async function generate( ...@@ -87,8 +87,8 @@ export async function generate(
* @returns Success confirmation * @returns Success confirmation
*/ */
export async function deleteCode(id: number): Promise<{ message: string }> { export async function deleteCode(id: number): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>(`/admin/redeem-codes/${id}`); const { data } = await apiClient.delete<{ message: string }>(`/admin/redeem-codes/${id}`)
return data; return data
} }
/** /**
...@@ -97,14 +97,14 @@ export async function deleteCode(id: number): Promise<{ message: string }> { ...@@ -97,14 +97,14 @@ export async function deleteCode(id: number): Promise<{ message: string }> {
* @returns Success confirmation * @returns Success confirmation
*/ */
export async function batchDelete(ids: number[]): Promise<{ export async function batchDelete(ids: number[]): Promise<{
deleted: number; deleted: number
message: string; message: string
}> { }> {
const { data } = await apiClient.post<{ const { data } = await apiClient.post<{
deleted: number; deleted: number
message: string; message: string
}>('/admin/redeem-codes/batch-delete', { ids }); }>('/admin/redeem-codes/batch-delete', { ids })
return data; return data
} }
/** /**
...@@ -113,8 +113,8 @@ export async function batchDelete(ids: number[]): Promise<{ ...@@ -113,8 +113,8 @@ export async function batchDelete(ids: number[]): Promise<{
* @returns Updated redeem code * @returns Updated redeem code
*/ */
export async function expire(id: number): Promise<RedeemCode> { export async function expire(id: number): Promise<RedeemCode> {
const { data } = await apiClient.post<RedeemCode>(`/admin/redeem-codes/${id}/expire`); const { data } = await apiClient.post<RedeemCode>(`/admin/redeem-codes/${id}/expire`)
return data; return data
} }
/** /**
...@@ -122,22 +122,22 @@ export async function expire(id: number): Promise<RedeemCode> { ...@@ -122,22 +122,22 @@ export async function expire(id: number): Promise<RedeemCode> {
* @returns Statistics about redeem codes * @returns Statistics about redeem codes
*/ */
export async function getStats(): Promise<{ export async function getStats(): Promise<{
total_codes: number; total_codes: number
active_codes: number; active_codes: number
used_codes: number; used_codes: number
expired_codes: number; expired_codes: number
total_value_distributed: number; total_value_distributed: number
by_type: Record<RedeemCodeType, number>; by_type: Record<RedeemCodeType, number>
}> { }> {
const { data } = await apiClient.get<{ const { data } = await apiClient.get<{
total_codes: number; total_codes: number
active_codes: number; active_codes: number
used_codes: number; used_codes: number
expired_codes: number; expired_codes: number
total_value_distributed: number; total_value_distributed: number
by_type: Record<RedeemCodeType, number>; by_type: Record<RedeemCodeType, number>
}>('/admin/redeem-codes/stats'); }>('/admin/redeem-codes/stats')
return data; return data
} }
/** /**
...@@ -146,14 +146,14 @@ export async function getStats(): Promise<{ ...@@ -146,14 +146,14 @@ export async function getStats(): Promise<{
* @returns CSV data as blob * @returns CSV data as blob
*/ */
export async function exportCodes(filters?: { export async function exportCodes(filters?: {
type?: RedeemCodeType; type?: RedeemCodeType
status?: 'active' | 'used' | 'expired'; status?: 'active' | 'used' | 'expired'
}): Promise<Blob> { }): Promise<Blob> {
const response = await apiClient.get('/admin/redeem-codes/export', { const response = await apiClient.get('/admin/redeem-codes/export', {
params: filters, params: filters,
responseType: 'blob', responseType: 'blob'
}); })
return response.data; return response.data
} }
export const redeemAPI = { export const redeemAPI = {
...@@ -164,7 +164,7 @@ export const redeemAPI = { ...@@ -164,7 +164,7 @@ export const redeemAPI = {
batchDelete, batchDelete,
expire, expire,
getStats, getStats,
exportCodes, exportCodes
}; }
export default redeemAPI; export default redeemAPI
...@@ -3,37 +3,37 @@ ...@@ -3,37 +3,37 @@
* Handles system settings management for administrators * Handles system settings management for administrators
*/ */
import { apiClient } from '../client'; import { apiClient } from '../client'
/** /**
* System settings interface * System settings interface
*/ */
export interface SystemSettings { export interface SystemSettings {
// Registration settings // Registration settings
registration_enabled: boolean; registration_enabled: boolean
email_verify_enabled: boolean; email_verify_enabled: boolean
// Default settings // Default settings
default_balance: number; default_balance: number
default_concurrency: number; default_concurrency: number
// OEM settings // OEM settings
site_name: string; site_name: string
site_logo: string; site_logo: string
site_subtitle: string; site_subtitle: string
api_base_url: string; api_base_url: string
contact_info: string; contact_info: string
doc_url: string; doc_url: string
// SMTP settings // SMTP settings
smtp_host: string; smtp_host: string
smtp_port: number; smtp_port: number
smtp_username: string; smtp_username: string
smtp_password: string; smtp_password: string
smtp_from_email: string; smtp_from_email: string
smtp_from_name: string; smtp_from_name: string
smtp_use_tls: boolean; smtp_use_tls: boolean
// Cloudflare Turnstile settings // Cloudflare Turnstile settings
turnstile_enabled: boolean; turnstile_enabled: boolean
turnstile_site_key: string; turnstile_site_key: string
turnstile_secret_key: string; turnstile_secret_key: string
} }
/** /**
...@@ -41,8 +41,8 @@ export interface SystemSettings { ...@@ -41,8 +41,8 @@ export interface SystemSettings {
* @returns System settings * @returns System settings
*/ */
export async function getSettings(): Promise<SystemSettings> { export async function getSettings(): Promise<SystemSettings> {
const { data } = await apiClient.get<SystemSettings>('/admin/settings'); const { data } = await apiClient.get<SystemSettings>('/admin/settings')
return data; return data
} }
/** /**
...@@ -51,19 +51,19 @@ export async function getSettings(): Promise<SystemSettings> { ...@@ -51,19 +51,19 @@ export async function getSettings(): Promise<SystemSettings> {
* @returns Updated settings * @returns Updated settings
*/ */
export async function updateSettings(settings: Partial<SystemSettings>): Promise<SystemSettings> { export async function updateSettings(settings: Partial<SystemSettings>): Promise<SystemSettings> {
const { data } = await apiClient.put<SystemSettings>('/admin/settings', settings); const { data } = await apiClient.put<SystemSettings>('/admin/settings', settings)
return data; return data
} }
/** /**
* Test SMTP connection request * Test SMTP connection request
*/ */
export interface TestSmtpRequest { export interface TestSmtpRequest {
smtp_host: string; smtp_host: string
smtp_port: number; smtp_port: number
smtp_username: string; smtp_username: string
smtp_password: string; smtp_password: string
smtp_use_tls: boolean; smtp_use_tls: boolean
} }
/** /**
...@@ -72,22 +72,22 @@ export interface TestSmtpRequest { ...@@ -72,22 +72,22 @@ export interface TestSmtpRequest {
* @returns Test result message * @returns Test result message
*/ */
export async function testSmtpConnection(config: TestSmtpRequest): Promise<{ message: string }> { export async function testSmtpConnection(config: TestSmtpRequest): Promise<{ message: string }> {
const { data } = await apiClient.post<{ message: string }>('/admin/settings/test-smtp', config); const { data } = await apiClient.post<{ message: string }>('/admin/settings/test-smtp', config)
return data; return data
} }
/** /**
* Send test email request * Send test email request
*/ */
export interface SendTestEmailRequest { export interface SendTestEmailRequest {
email: string; email: string
smtp_host: string; smtp_host: string
smtp_port: number; smtp_port: number
smtp_username: string; smtp_username: string
smtp_password: string; smtp_password: string
smtp_from_email: string; smtp_from_email: string
smtp_from_name: string; smtp_from_name: string
smtp_use_tls: boolean; smtp_use_tls: boolean
} }
/** /**
...@@ -96,16 +96,19 @@ export interface SendTestEmailRequest { ...@@ -96,16 +96,19 @@ export interface SendTestEmailRequest {
* @returns Test result message * @returns Test result message
*/ */
export async function sendTestEmail(request: SendTestEmailRequest): Promise<{ message: string }> { export async function sendTestEmail(request: SendTestEmailRequest): Promise<{ message: string }> {
const { data } = await apiClient.post<{ message: string }>('/admin/settings/send-test-email', request); const { data } = await apiClient.post<{ message: string }>(
return data; '/admin/settings/send-test-email',
request
)
return data
} }
/** /**
* Admin API Key status response * Admin API Key status response
*/ */
export interface AdminApiKeyStatus { export interface AdminApiKeyStatus {
exists: boolean; exists: boolean
masked_key: string; masked_key: string
} }
/** /**
...@@ -113,8 +116,8 @@ export interface AdminApiKeyStatus { ...@@ -113,8 +116,8 @@ export interface AdminApiKeyStatus {
* @returns Status indicating if key exists and masked version * @returns Status indicating if key exists and masked version
*/ */
export async function getAdminApiKey(): Promise<AdminApiKeyStatus> { export async function getAdminApiKey(): Promise<AdminApiKeyStatus> {
const { data } = await apiClient.get<AdminApiKeyStatus>('/admin/settings/admin-api-key'); const { data } = await apiClient.get<AdminApiKeyStatus>('/admin/settings/admin-api-key')
return data; return data
} }
/** /**
...@@ -122,8 +125,8 @@ export async function getAdminApiKey(): Promise<AdminApiKeyStatus> { ...@@ -122,8 +125,8 @@ export async function getAdminApiKey(): Promise<AdminApiKeyStatus> {
* @returns The new full API key (only shown once) * @returns The new full API key (only shown once)
*/ */
export async function regenerateAdminApiKey(): Promise<{ key: string }> { export async function regenerateAdminApiKey(): Promise<{ key: string }> {
const { data } = await apiClient.post<{ key: string }>('/admin/settings/admin-api-key/regenerate'); const { data } = await apiClient.post<{ key: string }>('/admin/settings/admin-api-key/regenerate')
return data; return data
} }
/** /**
...@@ -131,8 +134,8 @@ export async function regenerateAdminApiKey(): Promise<{ key: string }> { ...@@ -131,8 +134,8 @@ export async function regenerateAdminApiKey(): Promise<{ key: string }> {
* @returns Success message * @returns Success message
*/ */
export async function deleteAdminApiKey(): Promise<{ message: string }> { export async function deleteAdminApiKey(): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>('/admin/settings/admin-api-key'); const { data } = await apiClient.delete<{ message: string }>('/admin/settings/admin-api-key')
return data; return data
} }
export const settingsAPI = { export const settingsAPI = {
...@@ -142,7 +145,7 @@ export const settingsAPI = { ...@@ -142,7 +145,7 @@ export const settingsAPI = {
sendTestEmail, sendTestEmail,
getAdminApiKey, getAdminApiKey,
regenerateAdminApiKey, regenerateAdminApiKey,
deleteAdminApiKey, deleteAdminApiKey
}; }
export default settingsAPI; export default settingsAPI
...@@ -3,15 +3,15 @@ ...@@ -3,15 +3,15 @@
* Handles user subscription management for administrators * Handles user subscription management for administrators
*/ */
import { apiClient } from '../client'; import { apiClient } from '../client'
import type { import type {
UserSubscription, UserSubscription,
SubscriptionProgress, SubscriptionProgress,
AssignSubscriptionRequest, AssignSubscriptionRequest,
BulkAssignSubscriptionRequest, BulkAssignSubscriptionRequest,
ExtendSubscriptionRequest, ExtendSubscriptionRequest,
PaginatedResponse, PaginatedResponse
} from '@/types'; } from '@/types'
/** /**
* List all subscriptions with pagination * List all subscriptions with pagination
...@@ -24,19 +24,22 @@ export async function list( ...@@ -24,19 +24,22 @@ export async function list(
page: number = 1, page: number = 1,
pageSize: number = 20, pageSize: number = 20,
filters?: { filters?: {
status?: 'active' | 'expired' | 'revoked'; status?: 'active' | 'expired' | 'revoked'
user_id?: number; user_id?: number
group_id?: number; group_id?: number
} }
): Promise<PaginatedResponse<UserSubscription>> { ): Promise<PaginatedResponse<UserSubscription>> {
const { data } = await apiClient.get<PaginatedResponse<UserSubscription>>('/admin/subscriptions', { const { data } = await apiClient.get<PaginatedResponse<UserSubscription>>(
params: { '/admin/subscriptions',
page, {
page_size: pageSize, params: {
...filters, page,
}, page_size: pageSize,
}); ...filters
return data; }
}
)
return data
} }
/** /**
...@@ -45,8 +48,8 @@ export async function list( ...@@ -45,8 +48,8 @@ export async function list(
* @returns Subscription details * @returns Subscription details
*/ */
export async function getById(id: number): Promise<UserSubscription> { export async function getById(id: number): Promise<UserSubscription> {
const { data } = await apiClient.get<UserSubscription>(`/admin/subscriptions/${id}`); const { data } = await apiClient.get<UserSubscription>(`/admin/subscriptions/${id}`)
return data; return data
} }
/** /**
...@@ -55,8 +58,8 @@ export async function getById(id: number): Promise<UserSubscription> { ...@@ -55,8 +58,8 @@ export async function getById(id: number): Promise<UserSubscription> {
* @returns Subscription progress with usage stats * @returns Subscription progress with usage stats
*/ */
export async function getProgress(id: number): Promise<SubscriptionProgress> { export async function getProgress(id: number): Promise<SubscriptionProgress> {
const { data } = await apiClient.get<SubscriptionProgress>(`/admin/subscriptions/${id}/progress`); const { data } = await apiClient.get<SubscriptionProgress>(`/admin/subscriptions/${id}/progress`)
return data; return data
} }
/** /**
...@@ -65,8 +68,8 @@ export async function getProgress(id: number): Promise<SubscriptionProgress> { ...@@ -65,8 +68,8 @@ export async function getProgress(id: number): Promise<SubscriptionProgress> {
* @returns Created subscription * @returns Created subscription
*/ */
export async function assign(request: AssignSubscriptionRequest): Promise<UserSubscription> { export async function assign(request: AssignSubscriptionRequest): Promise<UserSubscription> {
const { data } = await apiClient.post<UserSubscription>('/admin/subscriptions/assign', request); const { data } = await apiClient.post<UserSubscription>('/admin/subscriptions/assign', request)
return data; return data
} }
/** /**
...@@ -74,9 +77,14 @@ export async function assign(request: AssignSubscriptionRequest): Promise<UserSu ...@@ -74,9 +77,14 @@ export async function assign(request: AssignSubscriptionRequest): Promise<UserSu
* @param request - Bulk assignment request * @param request - Bulk assignment request
* @returns Created subscriptions * @returns Created subscriptions
*/ */
export async function bulkAssign(request: BulkAssignSubscriptionRequest): Promise<UserSubscription[]> { export async function bulkAssign(
const { data } = await apiClient.post<UserSubscription[]>('/admin/subscriptions/bulk-assign', request); request: BulkAssignSubscriptionRequest
return data; ): Promise<UserSubscription[]> {
const { data } = await apiClient.post<UserSubscription[]>(
'/admin/subscriptions/bulk-assign',
request
)
return data
} }
/** /**
...@@ -85,9 +93,15 @@ export async function bulkAssign(request: BulkAssignSubscriptionRequest): Promis ...@@ -85,9 +93,15 @@ export async function bulkAssign(request: BulkAssignSubscriptionRequest): Promis
* @param request - Extension request with days * @param request - Extension request with days
* @returns Updated subscription * @returns Updated subscription
*/ */
export async function extend(id: number, request: ExtendSubscriptionRequest): Promise<UserSubscription> { export async function extend(
const { data } = await apiClient.post<UserSubscription>(`/admin/subscriptions/${id}/extend`, request); id: number,
return data; request: ExtendSubscriptionRequest
): Promise<UserSubscription> {
const { data } = await apiClient.post<UserSubscription>(
`/admin/subscriptions/${id}/extend`,
request
)
return data
} }
/** /**
...@@ -96,8 +110,8 @@ export async function extend(id: number, request: ExtendSubscriptionRequest): Pr ...@@ -96,8 +110,8 @@ export async function extend(id: number, request: ExtendSubscriptionRequest): Pr
* @returns Success confirmation * @returns Success confirmation
*/ */
export async function revoke(id: number): Promise<{ message: string }> { export async function revoke(id: number): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>(`/admin/subscriptions/${id}`); const { data } = await apiClient.delete<{ message: string }>(`/admin/subscriptions/${id}`)
return data; return data
} }
/** /**
...@@ -115,10 +129,10 @@ export async function listByGroup( ...@@ -115,10 +129,10 @@ export async function listByGroup(
const { data } = await apiClient.get<PaginatedResponse<UserSubscription>>( const { data } = await apiClient.get<PaginatedResponse<UserSubscription>>(
`/admin/groups/${groupId}/subscriptions`, `/admin/groups/${groupId}/subscriptions`,
{ {
params: { page, page_size: pageSize }, params: { page, page_size: pageSize }
} }
); )
return data; return data
} }
/** /**
...@@ -136,10 +150,10 @@ export async function listByUser( ...@@ -136,10 +150,10 @@ export async function listByUser(
const { data } = await apiClient.get<PaginatedResponse<UserSubscription>>( const { data } = await apiClient.get<PaginatedResponse<UserSubscription>>(
`/admin/users/${userId}/subscriptions`, `/admin/users/${userId}/subscriptions`,
{ {
params: { page, page_size: pageSize }, params: { page, page_size: pageSize }
} }
); )
return data; return data
} }
export const subscriptionsAPI = { export const subscriptionsAPI = {
...@@ -151,7 +165,7 @@ export const subscriptionsAPI = { ...@@ -151,7 +165,7 @@ export const subscriptionsAPI = {
extend, extend,
revoke, revoke,
listByGroup, listByGroup,
listByUser, listByUser
}; }
export default subscriptionsAPI; export default subscriptionsAPI
...@@ -2,31 +2,31 @@ ...@@ -2,31 +2,31 @@
* System API endpoints for admin operations * System API endpoints for admin operations
*/ */
import { apiClient } from '../client'; import { apiClient } from '../client'
export interface ReleaseInfo { export interface ReleaseInfo {
name: string; name: string
body: string; body: string
published_at: string; published_at: string
html_url: string; html_url: string
} }
export interface VersionInfo { export interface VersionInfo {
current_version: string; current_version: string
latest_version: string; latest_version: string
has_update: boolean; has_update: boolean
release_info?: ReleaseInfo; release_info?: ReleaseInfo
cached: boolean; cached: boolean
warning?: string; warning?: string
build_type: string; // "source" for manual builds, "release" for CI builds build_type: string // "source" for manual builds, "release" for CI builds
} }
/** /**
* Get current version * Get current version
*/ */
export async function getVersion(): Promise<{ version: string }> { export async function getVersion(): Promise<{ version: string }> {
const { data } = await apiClient.get<{ version: string }>('/admin/system/version'); const { data } = await apiClient.get<{ version: string }>('/admin/system/version')
return data; return data
} }
/** /**
...@@ -35,14 +35,14 @@ export async function getVersion(): Promise<{ version: string }> { ...@@ -35,14 +35,14 @@ export async function getVersion(): Promise<{ version: string }> {
*/ */
export async function checkUpdates(force = false): Promise<VersionInfo> { export async function checkUpdates(force = false): Promise<VersionInfo> {
const { data } = await apiClient.get<VersionInfo>('/admin/system/check-updates', { const { data } = await apiClient.get<VersionInfo>('/admin/system/check-updates', {
params: force ? { force: 'true' } : undefined, params: force ? { force: 'true' } : undefined
}); })
return data; return data
} }
export interface UpdateResult { export interface UpdateResult {
message: string; message: string
need_restart: boolean; need_restart: boolean
} }
/** /**
...@@ -50,24 +50,24 @@ export interface UpdateResult { ...@@ -50,24 +50,24 @@ export interface UpdateResult {
* Downloads and applies the latest version * Downloads and applies the latest version
*/ */
export async function performUpdate(): Promise<UpdateResult> { export async function performUpdate(): Promise<UpdateResult> {
const { data } = await apiClient.post<UpdateResult>('/admin/system/update'); const { data } = await apiClient.post<UpdateResult>('/admin/system/update')
return data; return data
} }
/** /**
* Rollback to previous version * Rollback to previous version
*/ */
export async function rollback(): Promise<UpdateResult> { export async function rollback(): Promise<UpdateResult> {
const { data } = await apiClient.post<UpdateResult>('/admin/system/rollback'); const { data } = await apiClient.post<UpdateResult>('/admin/system/rollback')
return data; return data
} }
/** /**
* Restart the service * Restart the service
*/ */
export async function restartService(): Promise<{ message: string }> { export async function restartService(): Promise<{ message: string }> {
const { data } = await apiClient.post<{ message: string }>('/admin/system/restart'); const { data } = await apiClient.post<{ message: string }>('/admin/system/restart')
return data; return data
} }
export const systemAPI = { export const systemAPI = {
...@@ -75,7 +75,7 @@ export const systemAPI = { ...@@ -75,7 +75,7 @@ export const systemAPI = {
checkUpdates, checkUpdates,
performUpdate, performUpdate,
rollback, rollback,
restartService, restartService
}; }
export default systemAPI; export default systemAPI
...@@ -3,39 +3,35 @@ ...@@ -3,39 +3,35 @@
* Handles admin-level usage logs and statistics retrieval * Handles admin-level usage logs and statistics retrieval
*/ */
import { apiClient } from '../client'; import { apiClient } from '../client'
import type { import type { UsageLog, UsageQueryParams, PaginatedResponse } from '@/types'
UsageLog,
UsageQueryParams,
PaginatedResponse,
} from '@/types';
// ==================== Types ==================== // ==================== Types ====================
export interface AdminUsageStatsResponse { export interface AdminUsageStatsResponse {
total_requests: number; total_requests: number
total_input_tokens: number; total_input_tokens: number
total_output_tokens: number; total_output_tokens: number
total_cache_tokens: number; total_cache_tokens: number
total_tokens: number; total_tokens: number
total_cost: number; total_cost: number
total_actual_cost: number; total_actual_cost: number
average_duration_ms: number; average_duration_ms: number
} }
export interface SimpleUser { export interface SimpleUser {
id: number; id: number
email: string; email: string
} }
export interface SimpleApiKey { export interface SimpleApiKey {
id: number; id: number
name: string; name: string
user_id: number; user_id: number
} }
export interface AdminUsageQueryParams extends UsageQueryParams { export interface AdminUsageQueryParams extends UsageQueryParams {
user_id?: number; user_id?: number
} }
// ==================== API Functions ==================== // ==================== API Functions ====================
...@@ -47,9 +43,9 @@ export interface AdminUsageQueryParams extends UsageQueryParams { ...@@ -47,9 +43,9 @@ export interface AdminUsageQueryParams extends UsageQueryParams {
*/ */
export async function list(params: AdminUsageQueryParams): Promise<PaginatedResponse<UsageLog>> { export async function list(params: AdminUsageQueryParams): Promise<PaginatedResponse<UsageLog>> {
const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/admin/usage', { const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/admin/usage', {
params, params
}); })
return data; return data
} }
/** /**
...@@ -58,16 +54,16 @@ export async function list(params: AdminUsageQueryParams): Promise<PaginatedResp ...@@ -58,16 +54,16 @@ export async function list(params: AdminUsageQueryParams): Promise<PaginatedResp
* @returns Usage statistics * @returns Usage statistics
*/ */
export async function getStats(params: { export async function getStats(params: {
user_id?: number; user_id?: number
api_key_id?: number; api_key_id?: number
period?: string; period?: string
start_date?: string; start_date?: string
end_date?: string; end_date?: string
}): Promise<AdminUsageStatsResponse> { }): Promise<AdminUsageStatsResponse> {
const { data } = await apiClient.get<AdminUsageStatsResponse>('/admin/usage/stats', { const { data } = await apiClient.get<AdminUsageStatsResponse>('/admin/usage/stats', {
params, params
}); })
return data; return data
} }
/** /**
...@@ -77,9 +73,9 @@ export async function getStats(params: { ...@@ -77,9 +73,9 @@ export async function getStats(params: {
*/ */
export async function searchUsers(keyword: string): Promise<SimpleUser[]> { export async function searchUsers(keyword: string): Promise<SimpleUser[]> {
const { data } = await apiClient.get<SimpleUser[]>('/admin/usage/search-users', { const { data } = await apiClient.get<SimpleUser[]>('/admin/usage/search-users', {
params: { q: keyword }, params: { q: keyword }
}); })
return data; return data
} }
/** /**
...@@ -89,24 +85,24 @@ export async function searchUsers(keyword: string): Promise<SimpleUser[]> { ...@@ -89,24 +85,24 @@ export async function searchUsers(keyword: string): Promise<SimpleUser[]> {
* @returns List of matching API keys (max 30) * @returns List of matching API keys (max 30)
*/ */
export async function searchApiKeys(userId?: number, keyword?: string): Promise<SimpleApiKey[]> { export async function searchApiKeys(userId?: number, keyword?: string): Promise<SimpleApiKey[]> {
const params: Record<string, unknown> = {}; const params: Record<string, unknown> = {}
if (userId !== undefined) { if (userId !== undefined) {
params.user_id = userId; params.user_id = userId
} }
if (keyword) { if (keyword) {
params.q = keyword; params.q = keyword
} }
const { data } = await apiClient.get<SimpleApiKey[]>('/admin/usage/search-api-keys', { const { data } = await apiClient.get<SimpleApiKey[]>('/admin/usage/search-api-keys', {
params, params
}); })
return data; return data
} }
export const adminUsageAPI = { export const adminUsageAPI = {
list, list,
getStats, getStats,
searchUsers, searchUsers,
searchApiKeys, searchApiKeys
}; }
export default adminUsageAPI; export default adminUsageAPI
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
* Handles user management for administrators * Handles user management for administrators
*/ */
import { apiClient } from '../client'; import { apiClient } from '../client'
import type { User, UpdateUserRequest, PaginatedResponse } from '@/types'; import type { User, UpdateUserRequest, PaginatedResponse } from '@/types'
/** /**
* List all users with pagination * List all users with pagination
...@@ -17,19 +17,19 @@ export async function list( ...@@ -17,19 +17,19 @@ export async function list(
page: number = 1, page: number = 1,
pageSize: number = 20, pageSize: number = 20,
filters?: { filters?: {
status?: 'active' | 'disabled'; status?: 'active' | 'disabled'
role?: 'admin' | 'user'; role?: 'admin' | 'user'
search?: string; search?: string
} }
): Promise<PaginatedResponse<User>> { ): Promise<PaginatedResponse<User>> {
const { data } = await apiClient.get<PaginatedResponse<User>>('/admin/users', { const { data } = await apiClient.get<PaginatedResponse<User>>('/admin/users', {
params: { params: {
page, page,
page_size: pageSize, page_size: pageSize,
...filters, ...filters
}, }
}); })
return data; return data
} }
/** /**
...@@ -38,8 +38,8 @@ export async function list( ...@@ -38,8 +38,8 @@ export async function list(
* @returns User details * @returns User details
*/ */
export async function getById(id: number): Promise<User> { export async function getById(id: number): Promise<User> {
const { data } = await apiClient.get<User>(`/admin/users/${id}`); const { data } = await apiClient.get<User>(`/admin/users/${id}`)
return data; return data
} }
/** /**
...@@ -48,14 +48,14 @@ export async function getById(id: number): Promise<User> { ...@@ -48,14 +48,14 @@ export async function getById(id: number): Promise<User> {
* @returns Created user * @returns Created user
*/ */
export async function create(userData: { export async function create(userData: {
email: string; email: string
password: string; password: string
balance?: number; balance?: number
concurrency?: number; concurrency?: number
allowed_groups?: number[] | null; allowed_groups?: number[] | null
}): Promise<User> { }): Promise<User> {
const { data } = await apiClient.post<User>('/admin/users', userData); const { data } = await apiClient.post<User>('/admin/users', userData)
return data; return data
} }
/** /**
...@@ -65,8 +65,8 @@ export async function create(userData: { ...@@ -65,8 +65,8 @@ export async function create(userData: {
* @returns Updated user * @returns Updated user
*/ */
export async function update(id: number, updates: UpdateUserRequest): Promise<User> { export async function update(id: number, updates: UpdateUserRequest): Promise<User> {
const { data } = await apiClient.put<User>(`/admin/users/${id}`, updates); const { data } = await apiClient.put<User>(`/admin/users/${id}`, updates)
return data; return data
} }
/** /**
...@@ -75,8 +75,8 @@ export async function update(id: number, updates: UpdateUserRequest): Promise<Us ...@@ -75,8 +75,8 @@ export async function update(id: number, updates: UpdateUserRequest): Promise<Us
* @returns Success confirmation * @returns Success confirmation
*/ */
export async function deleteUser(id: number): Promise<{ message: string }> { export async function deleteUser(id: number): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>(`/admin/users/${id}`); const { data } = await apiClient.delete<{ message: string }>(`/admin/users/${id}`)
return data; return data
} }
/** /**
...@@ -96,9 +96,9 @@ export async function updateBalance( ...@@ -96,9 +96,9 @@ export async function updateBalance(
const { data } = await apiClient.post<User>(`/admin/users/${id}/balance`, { const { data } = await apiClient.post<User>(`/admin/users/${id}/balance`, {
balance, balance,
operation, operation,
notes: notes || '', notes: notes || ''
}); })
return data; return data
} }
/** /**
...@@ -108,7 +108,7 @@ export async function updateBalance( ...@@ -108,7 +108,7 @@ export async function updateBalance(
* @returns Updated user * @returns Updated user
*/ */
export async function updateConcurrency(id: number, concurrency: number): Promise<User> { export async function updateConcurrency(id: number, concurrency: number): Promise<User> {
return update(id, { concurrency }); return update(id, { concurrency })
} }
/** /**
...@@ -118,7 +118,7 @@ export async function updateConcurrency(id: number, concurrency: number): Promis ...@@ -118,7 +118,7 @@ export async function updateConcurrency(id: number, concurrency: number): Promis
* @returns Updated user * @returns Updated user
*/ */
export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise<User> { export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise<User> {
return update(id, { status }); return update(id, { status })
} }
/** /**
...@@ -127,8 +127,8 @@ export async function toggleStatus(id: number, status: 'active' | 'disabled'): P ...@@ -127,8 +127,8 @@ export async function toggleStatus(id: number, status: 'active' | 'disabled'): P
* @returns List of user's API keys * @returns List of user's API keys
*/ */
export async function getUserApiKeys(id: number): Promise<PaginatedResponse<any>> { export async function getUserApiKeys(id: number): Promise<PaginatedResponse<any>> {
const { data } = await apiClient.get<PaginatedResponse<any>>(`/admin/users/${id}/api-keys`); const { data } = await apiClient.get<PaginatedResponse<any>>(`/admin/users/${id}/api-keys`)
return data; return data
} }
/** /**
...@@ -141,18 +141,18 @@ export async function getUserUsageStats( ...@@ -141,18 +141,18 @@ export async function getUserUsageStats(
id: number, id: number,
period: string = 'month' period: string = 'month'
): Promise<{ ): Promise<{
total_requests: number; total_requests: number
total_cost: number; total_cost: number
total_tokens: number; total_tokens: number
}> { }> {
const { data } = await apiClient.get<{ const { data } = await apiClient.get<{
total_requests: number; total_requests: number
total_cost: number; total_cost: number
total_tokens: number; total_tokens: number
}>(`/admin/users/${id}/usage`, { }>(`/admin/users/${id}/usage`, {
params: { period }, params: { period }
}); })
return data; return data
} }
export const usersAPI = { export const usersAPI = {
...@@ -165,7 +165,7 @@ export const usersAPI = { ...@@ -165,7 +165,7 @@ export const usersAPI = {
updateConcurrency, updateConcurrency,
toggleStatus, toggleStatus,
getUserApiKeys, getUserApiKeys,
getUserUsageStats, getUserUsageStats
}; }
export default usersAPI; export default usersAPI
...@@ -3,29 +3,37 @@ ...@@ -3,29 +3,37 @@
* Handles user login, registration, and logout operations * Handles user login, registration, and logout operations
*/ */
import { apiClient } from './client'; import { apiClient } from './client'
import type { LoginRequest, RegisterRequest, AuthResponse, User, SendVerifyCodeRequest, SendVerifyCodeResponse, PublicSettings } from '@/types'; import type {
LoginRequest,
RegisterRequest,
AuthResponse,
User,
SendVerifyCodeRequest,
SendVerifyCodeResponse,
PublicSettings
} from '@/types'
/** /**
* Store authentication token in localStorage * Store authentication token in localStorage
*/ */
export function setAuthToken(token: string): void { export function setAuthToken(token: string): void {
localStorage.setItem('auth_token', token); localStorage.setItem('auth_token', token)
} }
/** /**
* Get authentication token from localStorage * Get authentication token from localStorage
*/ */
export function getAuthToken(): string | null { export function getAuthToken(): string | null {
return localStorage.getItem('auth_token'); return localStorage.getItem('auth_token')
} }
/** /**
* Clear authentication token from localStorage * Clear authentication token from localStorage
*/ */
export function clearAuthToken(): void { export function clearAuthToken(): void {
localStorage.removeItem('auth_token'); localStorage.removeItem('auth_token')
localStorage.removeItem('auth_user'); localStorage.removeItem('auth_user')
} }
/** /**
...@@ -34,13 +42,13 @@ export function clearAuthToken(): void { ...@@ -34,13 +42,13 @@ export function clearAuthToken(): void {
* @returns Authentication response with token and user data * @returns Authentication response with token and user data
*/ */
export async function login(credentials: LoginRequest): Promise<AuthResponse> { export async function login(credentials: LoginRequest): Promise<AuthResponse> {
const { data } = await apiClient.post<AuthResponse>('/auth/login', credentials); const { data } = await apiClient.post<AuthResponse>('/auth/login', credentials)
// Store token and user data // Store token and user data
setAuthToken(data.access_token); setAuthToken(data.access_token)
localStorage.setItem('auth_user', JSON.stringify(data.user)); localStorage.setItem('auth_user', JSON.stringify(data.user))
return data; return data
} }
/** /**
...@@ -49,13 +57,13 @@ export async function login(credentials: LoginRequest): Promise<AuthResponse> { ...@@ -49,13 +57,13 @@ export async function login(credentials: LoginRequest): Promise<AuthResponse> {
* @returns Authentication response with token and user data * @returns Authentication response with token and user data
*/ */
export async function register(userData: RegisterRequest): Promise<AuthResponse> { export async function register(userData: RegisterRequest): Promise<AuthResponse> {
const { data } = await apiClient.post<AuthResponse>('/auth/register', userData); const { data } = await apiClient.post<AuthResponse>('/auth/register', userData)
// Store token and user data // Store token and user data
setAuthToken(data.access_token); setAuthToken(data.access_token)
localStorage.setItem('auth_user', JSON.stringify(data.user)); localStorage.setItem('auth_user', JSON.stringify(data.user))
return data; return data
} }
/** /**
...@@ -63,8 +71,8 @@ export async function register(userData: RegisterRequest): Promise<AuthResponse> ...@@ -63,8 +71,8 @@ export async function register(userData: RegisterRequest): Promise<AuthResponse>
* @returns User profile data * @returns User profile data
*/ */
export async function getCurrentUser(): Promise<User> { export async function getCurrentUser(): Promise<User> {
const { data } = await apiClient.get<User>('/auth/me'); const { data } = await apiClient.get<User>('/auth/me')
return data; return data
} }
/** /**
...@@ -72,7 +80,7 @@ export async function getCurrentUser(): Promise<User> { ...@@ -72,7 +80,7 @@ export async function getCurrentUser(): Promise<User> {
* Clears authentication token and user data from localStorage * Clears authentication token and user data from localStorage
*/ */
export function logout(): void { export function logout(): void {
clearAuthToken(); clearAuthToken()
// Optionally redirect to login page // Optionally redirect to login page
// window.location.href = '/login'; // window.location.href = '/login';
} }
...@@ -82,7 +90,7 @@ export function logout(): void { ...@@ -82,7 +90,7 @@ export function logout(): void {
* @returns True if user has valid token * @returns True if user has valid token
*/ */
export function isAuthenticated(): boolean { export function isAuthenticated(): boolean {
return getAuthToken() !== null; return getAuthToken() !== null
} }
/** /**
...@@ -90,8 +98,8 @@ export function isAuthenticated(): boolean { ...@@ -90,8 +98,8 @@ export function isAuthenticated(): boolean {
* @returns Public settings including registration and Turnstile config * @returns Public settings including registration and Turnstile config
*/ */
export async function getPublicSettings(): Promise<PublicSettings> { export async function getPublicSettings(): Promise<PublicSettings> {
const { data } = await apiClient.get<PublicSettings>('/settings/public'); const { data } = await apiClient.get<PublicSettings>('/settings/public')
return data; return data
} }
/** /**
...@@ -99,9 +107,11 @@ export async function getPublicSettings(): Promise<PublicSettings> { ...@@ -99,9 +107,11 @@ export async function getPublicSettings(): Promise<PublicSettings> {
* @param request - Email and optional Turnstile token * @param request - Email and optional Turnstile token
* @returns Response with countdown seconds * @returns Response with countdown seconds
*/ */
export async function sendVerifyCode(request: SendVerifyCodeRequest): Promise<SendVerifyCodeResponse> { export async function sendVerifyCode(
const { data } = await apiClient.post<SendVerifyCodeResponse>('/auth/send-verify-code', request); request: SendVerifyCodeRequest
return data; ): Promise<SendVerifyCodeResponse> {
const { data } = await apiClient.post<SendVerifyCodeResponse>('/auth/send-verify-code', request)
return data
} }
export const authAPI = { export const authAPI = {
...@@ -114,7 +124,7 @@ export const authAPI = { ...@@ -114,7 +124,7 @@ export const authAPI = {
getAuthToken, getAuthToken,
clearAuthToken, clearAuthToken,
getPublicSettings, getPublicSettings,
sendVerifyCode, sendVerifyCode
}; }
export default authAPI; export default authAPI
...@@ -3,70 +3,70 @@ ...@@ -3,70 +3,70 @@
* Base client with interceptors for authentication and error handling * Base client with interceptors for authentication and error handling
*/ */
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'
import type { ApiResponse } from '@/types'; import type { ApiResponse } from '@/types'
// ==================== Axios Instance Configuration ==================== // ==================== Axios Instance Configuration ====================
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1'
export const apiClient: AxiosInstance = axios.create({ export const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
timeout: 30000, timeout: 30000,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, }
}); })
// ==================== Request Interceptor ==================== // ==================== Request Interceptor ====================
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
// Attach token from localStorage // Attach token from localStorage
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token')
if (token && config.headers) { if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`
} }
return config; return config
}, },
(error) => { (error) => {
return Promise.reject(error); return Promise.reject(error)
} }
); )
// ==================== Response Interceptor ==================== // ==================== Response Interceptor ====================
apiClient.interceptors.response.use( apiClient.interceptors.response.use(
(response) => { (response) => {
// Unwrap standard API response format { code, message, data } // Unwrap standard API response format { code, message, data }
const apiResponse = response.data as ApiResponse<unknown>; const apiResponse = response.data as ApiResponse<unknown>
if (apiResponse && typeof apiResponse === 'object' && 'code' in apiResponse) { if (apiResponse && typeof apiResponse === 'object' && 'code' in apiResponse) {
if (apiResponse.code === 0) { if (apiResponse.code === 0) {
// Success - return the data portion // Success - return the data portion
response.data = apiResponse.data; response.data = apiResponse.data
} else { } else {
// API error // API error
return Promise.reject({ return Promise.reject({
status: response.status, status: response.status,
code: apiResponse.code, code: apiResponse.code,
message: apiResponse.message || 'Unknown error', message: apiResponse.message || 'Unknown error'
}); })
} }
} }
return response; return response
}, },
(error: AxiosError<ApiResponse<unknown>>) => { (error: AxiosError<ApiResponse<unknown>>) => {
// Handle common errors // Handle common errors
if (error.response) { if (error.response) {
const { status, data } = error.response; const { status, data } = error.response
// 401: Unauthorized - clear token and redirect to login // 401: Unauthorized - clear token and redirect to login
if (status === 401) { if (status === 401) {
localStorage.removeItem('auth_token'); localStorage.removeItem('auth_token')
localStorage.removeItem('auth_user'); localStorage.removeItem('auth_user')
// Only redirect if not already on login page // Only redirect if not already on login page
if (!window.location.pathname.includes('/login')) { if (!window.location.pathname.includes('/login')) {
window.location.href = '/login'; window.location.href = '/login'
} }
} }
...@@ -74,16 +74,16 @@ apiClient.interceptors.response.use( ...@@ -74,16 +74,16 @@ apiClient.interceptors.response.use(
return Promise.reject({ return Promise.reject({
status, status,
code: data?.code, code: data?.code,
message: data?.message || error.message, message: data?.message || error.message
}); })
} }
// Network error // Network error
return Promise.reject({ return Promise.reject({
status: 0, status: 0,
message: 'Network error. Please check your connection.', message: 'Network error. Please check your connection.'
}); })
} }
); )
export default apiClient; export default apiClient
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
* Handles group-related operations for regular users * Handles group-related operations for regular users
*/ */
import { apiClient } from './client'; import { apiClient } from './client'
import type { Group } from '@/types'; import type { Group } from '@/types'
/** /**
* Get available groups that the current user can bind to API keys * Get available groups that the current user can bind to API keys
...@@ -14,12 +14,12 @@ import type { Group } from '@/types'; ...@@ -14,12 +14,12 @@ import type { Group } from '@/types';
* @returns List of available groups * @returns List of available groups
*/ */
export async function getAvailable(): Promise<Group[]> { export async function getAvailable(): Promise<Group[]> {
const { data } = await apiClient.get<Group[]>('/groups/available'); const { data } = await apiClient.get<Group[]>('/groups/available')
return data; return data
} }
export const userGroupsAPI = { export const userGroupsAPI = {
getAvailable, getAvailable
}; }
export default userGroupsAPI; export default userGroupsAPI
...@@ -4,20 +4,20 @@ ...@@ -4,20 +4,20 @@
*/ */
// Re-export the HTTP client // Re-export the HTTP client
export { apiClient } from './client'; export { apiClient } from './client'
// Auth API // Auth API
export { authAPI } from './auth'; export { authAPI } from './auth'
// User APIs // User APIs
export { keysAPI } from './keys'; export { keysAPI } from './keys'
export { usageAPI } from './usage'; export { usageAPI } from './usage'
export { userAPI } from './user'; export { userAPI } from './user'
export { redeemAPI, type RedeemHistoryItem } from './redeem'; export { redeemAPI, type RedeemHistoryItem } from './redeem'
export { userGroupsAPI } from './groups'; export { userGroupsAPI } from './groups'
// Admin APIs // Admin APIs
export { adminAPI } from './admin'; export { adminAPI } from './admin'
// Default export // Default export
export { default } from './client'; export { default } from './client'
...@@ -3,13 +3,8 @@ ...@@ -3,13 +3,8 @@
* Handles CRUD operations for user API keys * Handles CRUD operations for user API keys
*/ */
import { apiClient } from './client'; import { apiClient } from './client'
import type { import type { ApiKey, CreateApiKeyRequest, UpdateApiKeyRequest, PaginatedResponse } from '@/types'
ApiKey,
CreateApiKeyRequest,
UpdateApiKeyRequest,
PaginatedResponse,
} from '@/types';
/** /**
* List all API keys for current user * List all API keys for current user
...@@ -17,11 +12,14 @@ import type { ...@@ -17,11 +12,14 @@ import type {
* @param pageSize - Items per page (default: 10) * @param pageSize - Items per page (default: 10)
* @returns Paginated list of API keys * @returns Paginated list of API keys
*/ */
export async function list(page: number = 1, pageSize: number = 10): Promise<PaginatedResponse<ApiKey>> { export async function list(
page: number = 1,
pageSize: number = 10
): Promise<PaginatedResponse<ApiKey>> {
const { data } = await apiClient.get<PaginatedResponse<ApiKey>>('/keys', { const { data } = await apiClient.get<PaginatedResponse<ApiKey>>('/keys', {
params: { page, page_size: pageSize }, params: { page, page_size: pageSize }
}); })
return data; return data
} }
/** /**
...@@ -30,8 +28,8 @@ export async function list(page: number = 1, pageSize: number = 10): Promise<Pag ...@@ -30,8 +28,8 @@ export async function list(page: number = 1, pageSize: number = 10): Promise<Pag
* @returns API key details * @returns API key details
*/ */
export async function getById(id: number): Promise<ApiKey> { export async function getById(id: number): Promise<ApiKey> {
const { data } = await apiClient.get<ApiKey>(`/keys/${id}`); const { data } = await apiClient.get<ApiKey>(`/keys/${id}`)
return data; return data
} }
/** /**
...@@ -41,17 +39,21 @@ export async function getById(id: number): Promise<ApiKey> { ...@@ -41,17 +39,21 @@ export async function getById(id: number): Promise<ApiKey> {
* @param customKey - Optional custom key value * @param customKey - Optional custom key value
* @returns Created API key * @returns Created API key
*/ */
export async function create(name: string, groupId?: number | null, customKey?: string): Promise<ApiKey> { export async function create(
const payload: CreateApiKeyRequest = { name }; name: string,
groupId?: number | null,
customKey?: string
): Promise<ApiKey> {
const payload: CreateApiKeyRequest = { name }
if (groupId !== undefined) { if (groupId !== undefined) {
payload.group_id = groupId; payload.group_id = groupId
} }
if (customKey) { if (customKey) {
payload.custom_key = customKey; payload.custom_key = customKey
} }
const { data } = await apiClient.post<ApiKey>('/keys', payload); const { data } = await apiClient.post<ApiKey>('/keys', payload)
return data; return data
} }
/** /**
...@@ -61,8 +63,8 @@ export async function create(name: string, groupId?: number | null, customKey?: ...@@ -61,8 +63,8 @@ export async function create(name: string, groupId?: number | null, customKey?:
* @returns Updated API key * @returns Updated API key
*/ */
export async function update(id: number, updates: UpdateApiKeyRequest): Promise<ApiKey> { export async function update(id: number, updates: UpdateApiKeyRequest): Promise<ApiKey> {
const { data } = await apiClient.put<ApiKey>(`/keys/${id}`, updates); const { data } = await apiClient.put<ApiKey>(`/keys/${id}`, updates)
return data; return data
} }
/** /**
...@@ -71,8 +73,8 @@ export async function update(id: number, updates: UpdateApiKeyRequest): Promise< ...@@ -71,8 +73,8 @@ export async function update(id: number, updates: UpdateApiKeyRequest): Promise<
* @returns Success confirmation * @returns Success confirmation
*/ */
export async function deleteKey(id: number): Promise<{ message: string }> { export async function deleteKey(id: number): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>(`/keys/${id}`); const { data } = await apiClient.delete<{ message: string }>(`/keys/${id}`)
return data; return data
} }
/** /**
...@@ -81,11 +83,8 @@ export async function deleteKey(id: number): Promise<{ message: string }> { ...@@ -81,11 +83,8 @@ export async function deleteKey(id: number): Promise<{ message: string }> {
* @param status - New status * @param status - New status
* @returns Updated API key * @returns Updated API key
*/ */
export async function toggleStatus( export async function toggleStatus(id: number, status: 'active' | 'inactive'): Promise<ApiKey> {
id: number, return update(id, { status })
status: 'active' | 'inactive'
): Promise<ApiKey> {
return update(id, { status });
} }
export const keysAPI = { export const keysAPI = {
...@@ -94,7 +93,7 @@ export const keysAPI = { ...@@ -94,7 +93,7 @@ export const keysAPI = {
create, create,
update, update,
delete: deleteKey, delete: deleteKey,
toggleStatus, toggleStatus
}; }
export default keysAPI; export default keysAPI
...@@ -3,24 +3,24 @@ ...@@ -3,24 +3,24 @@
* Handles redeem code redemption for users * Handles redeem code redemption for users
*/ */
import { apiClient } from './client'; import { apiClient } from './client'
import type { RedeemCodeRequest } from '@/types'; import type { RedeemCodeRequest } from '@/types'
export interface RedeemHistoryItem { export interface RedeemHistoryItem {
id: number; id: number
code: string; code: string
type: string; type: string
value: number; value: number
status: string; status: string
used_at: string; used_at: string
created_at: string; created_at: string
// 订阅类型专用字段 // 订阅类型专用字段
group_id?: number; group_id?: number
validity_days?: number; validity_days?: number
group?: { group?: {
id: number; id: number
name: string; name: string
}; }
} }
/** /**
...@@ -29,23 +29,23 @@ export interface RedeemHistoryItem { ...@@ -29,23 +29,23 @@ export interface RedeemHistoryItem {
* @returns Redemption result with updated balance or concurrency * @returns Redemption result with updated balance or concurrency
*/ */
export async function redeem(code: string): Promise<{ export async function redeem(code: string): Promise<{
message: string; message: string
type: string; type: string
value: number; value: number
new_balance?: number; new_balance?: number
new_concurrency?: number; new_concurrency?: number
}> { }> {
const payload: RedeemCodeRequest = { code }; const payload: RedeemCodeRequest = { code }
const { data } = await apiClient.post<{ const { data } = await apiClient.post<{
message: string; message: string
type: string; type: string
value: number; value: number
new_balance?: number; new_balance?: number
new_concurrency?: number; new_concurrency?: number
}>('/redeem', payload); }>('/redeem', payload)
return data; return data
} }
/** /**
...@@ -53,13 +53,13 @@ export async function redeem(code: string): Promise<{ ...@@ -53,13 +53,13 @@ export async function redeem(code: string): Promise<{
* @returns List of redeemed codes * @returns List of redeemed codes
*/ */
export async function getHistory(): Promise<RedeemHistoryItem[]> { export async function getHistory(): Promise<RedeemHistoryItem[]> {
const { data } = await apiClient.get<RedeemHistoryItem[]>('/redeem/history'); const { data } = await apiClient.get<RedeemHistoryItem[]>('/redeem/history')
return data; return data
} }
export const redeemAPI = { export const redeemAPI = {
redeem, redeem,
getHistory, getHistory
}; }
export default redeemAPI; export default redeemAPI
/** /**
* Setup API endpoints * Setup API endpoints
*/ */
import axios from 'axios'; import axios from 'axios'
// Create a separate client for setup endpoints (not under /api/v1) // Create a separate client for setup endpoints (not under /api/v1)
const setupClient = axios.create({ const setupClient = axios.create({
baseURL: '', baseURL: '',
timeout: 30000, timeout: 30000,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, }
}); })
export interface SetupStatus { export interface SetupStatus {
needs_setup: boolean; needs_setup: boolean
step: string; step: string
} }
export interface DatabaseConfig { export interface DatabaseConfig {
host: string; host: string
port: number; port: number
user: string; user: string
password: string; password: string
dbname: string; dbname: string
sslmode: string; sslmode: string
} }
export interface RedisConfig { export interface RedisConfig {
host: string; host: string
port: number; port: number
password: string; password: string
db: number; db: number
} }
export interface AdminConfig { export interface AdminConfig {
email: string; email: string
password: string; password: string
} }
export interface ServerConfig { export interface ServerConfig {
host: string; host: string
port: number; port: number
mode: string; mode: string
} }
export interface InstallRequest { export interface InstallRequest {
database: DatabaseConfig; database: DatabaseConfig
redis: RedisConfig; redis: RedisConfig
admin: AdminConfig; admin: AdminConfig
server: ServerConfig; server: ServerConfig
} }
export interface InstallResponse { export interface InstallResponse {
message: string; message: string
restart: boolean; restart: boolean
} }
/** /**
* Get setup status * Get setup status
*/ */
export async function getSetupStatus(): Promise<SetupStatus> { export async function getSetupStatus(): Promise<SetupStatus> {
const response = await setupClient.get('/setup/status'); const response = await setupClient.get('/setup/status')
return response.data.data; return response.data.data
} }
/** /**
* Test database connection * Test database connection
*/ */
export async function testDatabase(config: DatabaseConfig): Promise<void> { export async function testDatabase(config: DatabaseConfig): Promise<void> {
await setupClient.post('/setup/test-db', config); await setupClient.post('/setup/test-db', config)
} }
/** /**
* Test Redis connection * Test Redis connection
*/ */
export async function testRedis(config: RedisConfig): Promise<void> { export async function testRedis(config: RedisConfig): Promise<void> {
await setupClient.post('/setup/test-redis', config); await setupClient.post('/setup/test-redis', config)
} }
/** /**
* Perform installation * Perform installation
*/ */
export async function install(config: InstallRequest): Promise<InstallResponse> { export async function install(config: InstallRequest): Promise<InstallResponse> {
const response = await setupClient.post('/setup/install', config); const response = await setupClient.post('/setup/install', config)
return response.data.data; return response.data.data
} }
...@@ -3,64 +3,68 @@ ...@@ -3,64 +3,68 @@
* API for regular users to view their own subscriptions and progress * API for regular users to view their own subscriptions and progress
*/ */
import { apiClient } from './client'; import { apiClient } from './client'
import type { UserSubscription, SubscriptionProgress } from '@/types'; import type { UserSubscription, SubscriptionProgress } from '@/types'
/** /**
* Subscription summary for user dashboard * Subscription summary for user dashboard
*/ */
export interface SubscriptionSummary { export interface SubscriptionSummary {
active_count: number; active_count: number
subscriptions: Array<{ subscriptions: Array<{
id: number; id: number
group_name: string; group_name: string
status: string; status: string
daily_progress: number | null; daily_progress: number | null
weekly_progress: number | null; weekly_progress: number | null
monthly_progress: number | null; monthly_progress: number | null
expires_at: string | null; expires_at: string | null
days_remaining: number | null; days_remaining: number | null
}>; }>
} }
/** /**
* Get list of current user's subscriptions * Get list of current user's subscriptions
*/ */
export async function getMySubscriptions(): Promise<UserSubscription[]> { export async function getMySubscriptions(): Promise<UserSubscription[]> {
const response = await apiClient.get<UserSubscription[]>('/subscriptions'); const response = await apiClient.get<UserSubscription[]>('/subscriptions')
return response.data; return response.data
} }
/** /**
* Get current user's active subscriptions * Get current user's active subscriptions
*/ */
export async function getActiveSubscriptions(): Promise<UserSubscription[]> { export async function getActiveSubscriptions(): Promise<UserSubscription[]> {
const response = await apiClient.get<UserSubscription[]>('/subscriptions/active'); const response = await apiClient.get<UserSubscription[]>('/subscriptions/active')
return response.data; return response.data
} }
/** /**
* Get progress for all user's active subscriptions * Get progress for all user's active subscriptions
*/ */
export async function getSubscriptionsProgress(): Promise<SubscriptionProgress[]> { export async function getSubscriptionsProgress(): Promise<SubscriptionProgress[]> {
const response = await apiClient.get<SubscriptionProgress[]>('/subscriptions/progress'); const response = await apiClient.get<SubscriptionProgress[]>('/subscriptions/progress')
return response.data; return response.data
} }
/** /**
* Get subscription summary for dashboard display * Get subscription summary for dashboard display
*/ */
export async function getSubscriptionSummary(): Promise<SubscriptionSummary> { export async function getSubscriptionSummary(): Promise<SubscriptionSummary> {
const response = await apiClient.get<SubscriptionSummary>('/subscriptions/summary'); const response = await apiClient.get<SubscriptionSummary>('/subscriptions/summary')
return response.data; return response.data
} }
/** /**
* Get progress for a specific subscription * Get progress for a specific subscription
*/ */
export async function getSubscriptionProgress(subscriptionId: number): Promise<SubscriptionProgress> { export async function getSubscriptionProgress(
const response = await apiClient.get<SubscriptionProgress>(`/subscriptions/${subscriptionId}/progress`); subscriptionId: number
return response.data; ): Promise<SubscriptionProgress> {
const response = await apiClient.get<SubscriptionProgress>(
`/subscriptions/${subscriptionId}/progress`
)
return response.data
} }
export default { export default {
...@@ -68,5 +72,5 @@ export default { ...@@ -68,5 +72,5 @@ export default {
getActiveSubscriptions, getActiveSubscriptions,
getSubscriptionsProgress, getSubscriptionsProgress,
getSubscriptionSummary, getSubscriptionSummary,
getSubscriptionProgress, getSubscriptionProgress
}; }
...@@ -3,59 +3,59 @@ ...@@ -3,59 +3,59 @@
* Handles usage logs and statistics retrieval * Handles usage logs and statistics retrieval
*/ */
import { apiClient } from './client'; import { apiClient } from './client'
import type { import type {
UsageLog, UsageLog,
UsageQueryParams, UsageQueryParams,
UsageStatsResponse, UsageStatsResponse,
PaginatedResponse, PaginatedResponse,
TrendDataPoint, TrendDataPoint,
ModelStat, ModelStat
} from '@/types'; } from '@/types'
// ==================== Dashboard Types ==================== // ==================== Dashboard Types ====================
export interface UserDashboardStats { export interface UserDashboardStats {
total_api_keys: number; total_api_keys: number
active_api_keys: number; active_api_keys: number
total_requests: number; total_requests: number
total_input_tokens: number; total_input_tokens: number
total_output_tokens: number; total_output_tokens: number
total_cache_creation_tokens: number; total_cache_creation_tokens: number
total_cache_read_tokens: number; total_cache_read_tokens: number
total_tokens: number; total_tokens: number
total_cost: number; // 标准计费 total_cost: number // 标准计费
total_actual_cost: number; // 实际扣除 total_actual_cost: number // 实际扣除
today_requests: number; today_requests: number
today_input_tokens: number; today_input_tokens: number
today_output_tokens: number; today_output_tokens: number
today_cache_creation_tokens: number; today_cache_creation_tokens: number
today_cache_read_tokens: number; today_cache_read_tokens: number
today_tokens: number; today_tokens: number
today_cost: number; // 今日标准计费 today_cost: number // 今日标准计费
today_actual_cost: number; // 今日实际扣除 today_actual_cost: number // 今日实际扣除
average_duration_ms: number; average_duration_ms: number
rpm: number; // 近5分钟平均每分钟请求数 rpm: number // 近5分钟平均每分钟请求数
tpm: number; // 近5分钟平均每分钟Token数 tpm: number // 近5分钟平均每分钟Token数
} }
export interface TrendParams { export interface TrendParams {
start_date?: string; start_date?: string
end_date?: string; end_date?: string
granularity?: 'day' | 'hour'; granularity?: 'day' | 'hour'
} }
export interface TrendResponse { export interface TrendResponse {
trend: TrendDataPoint[]; trend: TrendDataPoint[]
start_date: string; start_date: string
end_date: string; end_date: string
granularity: string; granularity: string
} }
export interface ModelStatsResponse { export interface ModelStatsResponse {
models: ModelStat[]; models: ModelStat[]
start_date: string; start_date: string
end_date: string; end_date: string
} }
/** /**
...@@ -72,17 +72,17 @@ export async function list( ...@@ -72,17 +72,17 @@ export async function list(
): Promise<PaginatedResponse<UsageLog>> { ): Promise<PaginatedResponse<UsageLog>> {
const params: UsageQueryParams = { const params: UsageQueryParams = {
page, page,
page_size: pageSize, page_size: pageSize
}; }
if (apiKeyId !== undefined) { if (apiKeyId !== undefined) {
params.api_key_id = apiKeyId; params.api_key_id = apiKeyId
} }
const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', { const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', {
params, params
}); })
return data; return data
} }
/** /**
...@@ -92,9 +92,9 @@ export async function list( ...@@ -92,9 +92,9 @@ export async function list(
*/ */
export async function query(params: UsageQueryParams): Promise<PaginatedResponse<UsageLog>> { export async function query(params: UsageQueryParams): Promise<PaginatedResponse<UsageLog>> {
const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', { const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', {
params, params
}); })
return data; return data
} }
/** /**
...@@ -107,16 +107,16 @@ export async function getStats( ...@@ -107,16 +107,16 @@ export async function getStats(
period: string = 'today', period: string = 'today',
apiKeyId?: number apiKeyId?: number
): Promise<UsageStatsResponse> { ): Promise<UsageStatsResponse> {
const params: Record<string, unknown> = { period }; const params: Record<string, unknown> = { period }
if (apiKeyId !== undefined) { if (apiKeyId !== undefined) {
params.api_key_id = apiKeyId; params.api_key_id = apiKeyId
} }
const { data } = await apiClient.get<UsageStatsResponse>('/usage/stats', { const { data } = await apiClient.get<UsageStatsResponse>('/usage/stats', {
params, params
}); })
return data; return data
} }
/** /**
...@@ -133,17 +133,17 @@ export async function getStatsByDateRange( ...@@ -133,17 +133,17 @@ export async function getStatsByDateRange(
): Promise<UsageStatsResponse> { ): Promise<UsageStatsResponse> {
const params: Record<string, unknown> = { const params: Record<string, unknown> = {
start_date: startDate, start_date: startDate,
end_date: endDate, end_date: endDate
}; }
if (apiKeyId !== undefined) { if (apiKeyId !== undefined) {
params.api_key_id = apiKeyId; params.api_key_id = apiKeyId
} }
const { data } = await apiClient.get<UsageStatsResponse>('/usage/stats', { const { data } = await apiClient.get<UsageStatsResponse>('/usage/stats', {
params, params
}); })
return data; return data
} }
/** /**
...@@ -162,17 +162,17 @@ export async function getByDateRange( ...@@ -162,17 +162,17 @@ export async function getByDateRange(
start_date: startDate, start_date: startDate,
end_date: endDate, end_date: endDate,
page: 1, page: 1,
page_size: 100, page_size: 100
}; }
if (apiKeyId !== undefined) { if (apiKeyId !== undefined) {
params.api_key_id = apiKeyId; params.api_key_id = apiKeyId
} }
const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', { const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', {
params, params
}); })
return data; return data
} }
/** /**
...@@ -181,8 +181,8 @@ export async function getByDateRange( ...@@ -181,8 +181,8 @@ export async function getByDateRange(
* @returns Usage log details * @returns Usage log details
*/ */
export async function getById(id: number): Promise<UsageLog> { export async function getById(id: number): Promise<UsageLog> {
const { data } = await apiClient.get<UsageLog>(`/usage/${id}`); const { data } = await apiClient.get<UsageLog>(`/usage/${id}`)
return data; return data
} }
// ==================== Dashboard API ==================== // ==================== Dashboard API ====================
...@@ -192,8 +192,8 @@ export async function getById(id: number): Promise<UsageLog> { ...@@ -192,8 +192,8 @@ export async function getById(id: number): Promise<UsageLog> {
* @returns Dashboard statistics for current user * @returns Dashboard statistics for current user
*/ */
export async function getDashboardStats(): Promise<UserDashboardStats> { export async function getDashboardStats(): Promise<UserDashboardStats> {
const { data } = await apiClient.get<UserDashboardStats>('/usage/dashboard/stats'); const { data } = await apiClient.get<UserDashboardStats>('/usage/dashboard/stats')
return data; return data
} }
/** /**
...@@ -202,8 +202,8 @@ export async function getDashboardStats(): Promise<UserDashboardStats> { ...@@ -202,8 +202,8 @@ export async function getDashboardStats(): Promise<UserDashboardStats> {
* @returns Usage trend data for current user * @returns Usage trend data for current user
*/ */
export async function getDashboardTrend(params?: TrendParams): Promise<TrendResponse> { export async function getDashboardTrend(params?: TrendParams): Promise<TrendResponse> {
const { data } = await apiClient.get<TrendResponse>('/usage/dashboard/trend', { params }); const { data } = await apiClient.get<TrendResponse>('/usage/dashboard/trend', { params })
return data; return data
} }
/** /**
...@@ -211,19 +211,22 @@ export async function getDashboardTrend(params?: TrendParams): Promise<TrendResp ...@@ -211,19 +211,22 @@ export async function getDashboardTrend(params?: TrendParams): Promise<TrendResp
* @param params - Query parameters for filtering * @param params - Query parameters for filtering
* @returns Model usage statistics for current user * @returns Model usage statistics for current user
*/ */
export async function getDashboardModels(params?: { start_date?: string; end_date?: string }): Promise<ModelStatsResponse> { export async function getDashboardModels(params?: {
const { data } = await apiClient.get<ModelStatsResponse>('/usage/dashboard/models', { params }); start_date?: string
return data; end_date?: string
}): Promise<ModelStatsResponse> {
const { data } = await apiClient.get<ModelStatsResponse>('/usage/dashboard/models', { params })
return data
} }
export interface BatchApiKeyUsageStats { export interface BatchApiKeyUsageStats {
api_key_id: number; api_key_id: number
today_actual_cost: number; today_actual_cost: number
total_actual_cost: number; total_actual_cost: number
} }
export interface BatchApiKeysUsageResponse { export interface BatchApiKeysUsageResponse {
stats: Record<string, BatchApiKeyUsageStats>; stats: Record<string, BatchApiKeyUsageStats>
} }
/** /**
...@@ -231,11 +234,16 @@ export interface BatchApiKeysUsageResponse { ...@@ -231,11 +234,16 @@ export interface BatchApiKeysUsageResponse {
* @param apiKeyIds - Array of API key IDs * @param apiKeyIds - Array of API key IDs
* @returns Usage stats map keyed by API key ID * @returns Usage stats map keyed by API key ID
*/ */
export async function getDashboardApiKeysUsage(apiKeyIds: number[]): Promise<BatchApiKeysUsageResponse> { export async function getDashboardApiKeysUsage(
const { data } = await apiClient.post<BatchApiKeysUsageResponse>('/usage/dashboard/api-keys-usage', { apiKeyIds: number[]
api_key_ids: apiKeyIds, ): Promise<BatchApiKeysUsageResponse> {
}); const { data } = await apiClient.post<BatchApiKeysUsageResponse>(
return data; '/usage/dashboard/api-keys-usage',
{
api_key_ids: apiKeyIds
}
)
return data
} }
export const usageAPI = { export const usageAPI = {
...@@ -249,7 +257,7 @@ export const usageAPI = { ...@@ -249,7 +257,7 @@ export const usageAPI = {
getDashboardStats, getDashboardStats,
getDashboardTrend, getDashboardTrend,
getDashboardModels, getDashboardModels,
getDashboardApiKeysUsage, getDashboardApiKeysUsage
}; }
export default usageAPI; export default usageAPI
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
* Handles user profile management and password changes * Handles user profile management and password changes
*/ */
import { apiClient } from './client'; import { apiClient } from './client'
import type { User, ChangePasswordRequest } from '@/types'; import type { User, ChangePasswordRequest } from '@/types'
/** /**
* Get current user profile * Get current user profile
* @returns User profile data * @returns User profile data
*/ */
export async function getProfile(): Promise<User> { export async function getProfile(): Promise<User> {
const { data } = await apiClient.get<User>('/user/profile'); const { data } = await apiClient.get<User>('/user/profile')
return data; return data
} }
/** /**
...@@ -21,11 +21,11 @@ export async function getProfile(): Promise<User> { ...@@ -21,11 +21,11 @@ export async function getProfile(): Promise<User> {
* @returns Updated user profile data * @returns Updated user profile data
*/ */
export async function updateProfile(profile: { export async function updateProfile(profile: {
username?: string; username?: string
wechat?: string; wechat?: string
}): Promise<User> { }): Promise<User> {
const { data } = await apiClient.put<User>('/user', profile); const { data } = await apiClient.put<User>('/user', profile)
return data; return data
} }
/** /**
...@@ -39,17 +39,17 @@ export async function changePassword( ...@@ -39,17 +39,17 @@ export async function changePassword(
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const payload: ChangePasswordRequest = { const payload: ChangePasswordRequest = {
old_password: oldPassword, old_password: oldPassword,
new_password: newPassword, new_password: newPassword
}; }
const { data } = await apiClient.put<{ message: string }>('/user/password', payload); const { data } = await apiClient.put<{ message: string }>('/user/password', payload)
return data; return data
} }
export const userAPI = { export const userAPI = {
getProfile, getProfile,
updateProfile, updateProfile,
changePassword, changePassword
}; }
export default userAPI; export default userAPI
...@@ -5,158 +5,164 @@ ...@@ -5,158 +5,164 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'; import { ref, onMounted, onUnmounted, watch } from 'vue'
interface TurnstileRenderOptions { interface TurnstileRenderOptions {
sitekey: string; sitekey: string
callback: (token: string) => void; callback: (token: string) => void
'expired-callback'?: () => void; 'expired-callback'?: () => void
'error-callback'?: () => void; 'error-callback'?: () => void
theme?: 'light' | 'dark' | 'auto'; theme?: 'light' | 'dark' | 'auto'
size?: 'normal' | 'compact' | 'flexible'; size?: 'normal' | 'compact' | 'flexible'
} }
interface TurnstileAPI { interface TurnstileAPI {
render: (container: HTMLElement, options: TurnstileRenderOptions) => string; render: (container: HTMLElement, options: TurnstileRenderOptions) => string
reset: (widgetId?: string) => void; reset: (widgetId?: string) => void
remove: (widgetId?: string) => void; remove: (widgetId?: string) => void
} }
declare global { declare global {
interface Window { interface Window {
turnstile?: TurnstileAPI; turnstile?: TurnstileAPI
onTurnstileLoad?: () => void; onTurnstileLoad?: () => void
} }
} }
const props = withDefaults(defineProps<{ const props = withDefaults(
siteKey: string; defineProps<{
theme?: 'light' | 'dark' | 'auto'; siteKey: string
size?: 'normal' | 'compact' | 'flexible'; theme?: 'light' | 'dark' | 'auto'
}>(), { size?: 'normal' | 'compact' | 'flexible'
theme: 'auto', }>(),
size: 'flexible', {
}); theme: 'auto',
size: 'flexible'
}
)
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'verify', token: string): void; (e: 'verify', token: string): void
(e: 'expire'): void; (e: 'expire'): void
(e: 'error'): void; (e: 'error'): void
}>(); }>()
const containerRef = ref<HTMLElement | null>(null); const containerRef = ref<HTMLElement | null>(null)
const widgetId = ref<string | null>(null); const widgetId = ref<string | null>(null)
const scriptLoaded = ref(false); const scriptLoaded = ref(false)
const loadScript = (): Promise<void> => { const loadScript = (): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (window.turnstile) { if (window.turnstile) {
scriptLoaded.value = true; scriptLoaded.value = true
resolve(); resolve()
return; return
} }
// Check if script is already loading // Check if script is already loading
const existingScript = document.querySelector('script[src*="turnstile"]'); const existingScript = document.querySelector('script[src*="turnstile"]')
if (existingScript) { if (existingScript) {
window.onTurnstileLoad = () => { window.onTurnstileLoad = () => {
scriptLoaded.value = true; scriptLoaded.value = true
resolve(); resolve()
}; }
return; return
} }
const script = document.createElement('script'); const script = document.createElement('script')
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileLoad'; script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileLoad'
script.async = true; script.async = true
script.defer = true; script.defer = true
window.onTurnstileLoad = () => { window.onTurnstileLoad = () => {
scriptLoaded.value = true; scriptLoaded.value = true
resolve(); resolve()
}; }
script.onerror = () => { script.onerror = () => {
reject(new Error('Failed to load Turnstile script')); reject(new Error('Failed to load Turnstile script'))
}; }
document.head.appendChild(script); document.head.appendChild(script)
}); })
}; }
const renderWidget = () => { const renderWidget = () => {
if (!window.turnstile || !containerRef.value || !props.siteKey) { if (!window.turnstile || !containerRef.value || !props.siteKey) {
return; return
} }
// Remove existing widget if any // Remove existing widget if any
if (widgetId.value) { if (widgetId.value) {
try { try {
window.turnstile.remove(widgetId.value); window.turnstile.remove(widgetId.value)
} catch { } catch {
// Ignore errors when removing // Ignore errors when removing
} }
widgetId.value = null; widgetId.value = null
} }
// Clear container // Clear container
containerRef.value.innerHTML = ''; containerRef.value.innerHTML = ''
widgetId.value = window.turnstile.render(containerRef.value, { widgetId.value = window.turnstile.render(containerRef.value, {
sitekey: props.siteKey, sitekey: props.siteKey,
callback: (token: string) => { callback: (token: string) => {
emit('verify', token); emit('verify', token)
}, },
'expired-callback': () => { 'expired-callback': () => {
emit('expire'); emit('expire')
}, },
'error-callback': () => { 'error-callback': () => {
emit('error'); emit('error')
}, },
theme: props.theme, theme: props.theme,
size: props.size, size: props.size
}); })
}; }
const reset = () => { const reset = () => {
if (window.turnstile && widgetId.value) { if (window.turnstile && widgetId.value) {
window.turnstile.reset(widgetId.value); window.turnstile.reset(widgetId.value)
} }
}; }
// Expose reset method to parent // Expose reset method to parent
defineExpose({ reset }); defineExpose({ reset })
onMounted(async () => { onMounted(async () => {
if (!props.siteKey) { if (!props.siteKey) {
return; return
} }
try { try {
await loadScript(); await loadScript()
renderWidget(); renderWidget()
} catch (error) { } catch (error) {
console.error('Failed to initialize Turnstile:', error); console.error('Failed to initialize Turnstile:', error)
emit('error'); emit('error')
} }
}); })
onUnmounted(() => { onUnmounted(() => {
if (window.turnstile && widgetId.value) { if (window.turnstile && widgetId.value) {
try { try {
window.turnstile.remove(widgetId.value); window.turnstile.remove(widgetId.value)
} catch { } catch {
// Ignore errors when removing // Ignore errors when removing
} }
} }
}); })
// Re-render when siteKey changes // Re-render when siteKey changes
watch(() => props.siteKey, (newKey) => { watch(
if (newKey && scriptLoaded.value) { () => props.siteKey,
renderWidget(); (newKey) => {
if (newKey && scriptLoaded.value) {
renderWidget()
}
} }
}); )
</script> </script>
<style scoped> <style scoped>
......
<template> <template>
<Modal <Modal :show="show" :title="t('admin.accounts.usageStatistics')" size="2xl" @close="handleClose">
:show="show"
:title="t('admin.accounts.usageStatistics')"
size="2xl"
@close="handleClose"
>
<div class="space-y-6"> <div class="space-y-6">
<!-- Account Info Header --> <!-- Account Info Header -->
<div v-if="account" class="flex items-center justify-between p-3 bg-gradient-to-r from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20 rounded-xl border border-primary-200 dark:border-primary-700/50"> <div
v-if="account"
class="flex items-center justify-between rounded-xl border border-primary-200 bg-gradient-to-r from-primary-50 to-primary-100 p-3 dark:border-primary-700/50 dark:from-primary-900/20 dark:to-primary-800/20"
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center"> <div
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> >
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg> </svg>
</div> </div>
<div> <div>
...@@ -23,7 +28,7 @@ ...@@ -23,7 +28,7 @@
</div> </div>
<span <span
:class="[ :class="[
'px-2.5 py-1 text-xs font-semibold rounded-full', 'rounded-full px-2.5 py-1 text-xs font-semibold',
account.status === 'active' account.status === 'active'
? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400' ? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400'
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400' : 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'
...@@ -42,62 +47,140 @@ ...@@ -42,62 +47,140 @@
<!-- Row 1: Main Stats Cards --> <!-- Row 1: Main Stats Cards -->
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4"> <div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<!-- 30-Day Total Cost --> <!-- 30-Day Total Cost -->
<div class="card p-4 bg-gradient-to-br from-emerald-50 to-white dark:from-emerald-900/10 dark:to-dark-700 border-emerald-200 dark:border-emerald-800/30"> <div
<div class="flex items-center justify-between mb-2"> class="card border-emerald-200 bg-gradient-to-br from-emerald-50 to-white p-4 dark:border-emerald-800/30 dark:from-emerald-900/10 dark:to-dark-700"
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.totalCost') }}</span> >
<div class="p-1.5 rounded-lg bg-emerald-100 dark:bg-emerald-900/30"> <div class="mb-2 flex items-center justify-between">
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> t('admin.accounts.stats.totalCost')
}}</span>
<div class="rounded-lg bg-emerald-100 p-1.5 dark:bg-emerald-900/30">
<svg
class="h-4 w-4 text-emerald-600 dark:text-emerald-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
</div> </div>
</div> </div>
<p class="text-2xl font-bold text-gray-900 dark:text-white">${{ formatCost(stats.summary.total_cost) }}</p> <p class="text-2xl font-bold text-gray-900 dark:text-white">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1"> ${{ formatCost(stats.summary.total_cost) }}
</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.stats.accumulatedCost') }} {{ t('admin.accounts.stats.accumulatedCost') }}
<span class="text-gray-400 dark:text-gray-500">({{ t('admin.accounts.stats.standardCost') }}: ${{ formatCost(stats.summary.total_standard_cost) }})</span> <span class="text-gray-400 dark:text-gray-500"
>({{ t('admin.accounts.stats.standardCost') }}: ${{
formatCost(stats.summary.total_standard_cost)
}})</span
>
</p> </p>
</div> </div>
<!-- 30-Day Total Requests --> <!-- 30-Day Total Requests -->
<div class="card p-4 bg-gradient-to-br from-blue-50 to-white dark:from-blue-900/10 dark:to-dark-700 border-blue-200 dark:border-blue-800/30"> <div
<div class="flex items-center justify-between mb-2"> class="card border-blue-200 bg-gradient-to-br from-blue-50 to-white p-4 dark:border-blue-800/30 dark:from-blue-900/10 dark:to-dark-700"
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.totalRequests') }}</span> >
<div class="p-1.5 rounded-lg bg-blue-100 dark:bg-blue-900/30"> <div class="mb-2 flex items-center justify-between">
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> t('admin.accounts.stats.totalRequests')
}}</span>
<div class="rounded-lg bg-blue-100 p-1.5 dark:bg-blue-900/30">
<svg
class="h-4 w-4 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg> </svg>
</div> </div>
</div> </div>
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ formatNumber(stats.summary.total_requests) }}</p> <p class="text-2xl font-bold text-gray-900 dark:text-white">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ t('admin.accounts.stats.totalCalls') }}</p> {{ formatNumber(stats.summary.total_requests) }}
</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.stats.totalCalls') }}
</p>
</div> </div>
<!-- Daily Average Cost --> <!-- Daily Average Cost -->
<div class="card p-4 bg-gradient-to-br from-amber-50 to-white dark:from-amber-900/10 dark:to-dark-700 border-amber-200 dark:border-amber-800/30"> <div
<div class="flex items-center justify-between mb-2"> class="card border-amber-200 bg-gradient-to-br from-amber-50 to-white p-4 dark:border-amber-800/30 dark:from-amber-900/10 dark:to-dark-700"
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.avgDailyCost') }}</span> >
<div class="p-1.5 rounded-lg bg-amber-100 dark:bg-amber-900/30"> <div class="mb-2 flex items-center justify-between">
<svg class="w-4 h-4 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" /> t('admin.accounts.stats.avgDailyCost')
}}</span>
<div class="rounded-lg bg-amber-100 p-1.5 dark:bg-amber-900/30">
<svg
class="h-4 w-4 text-amber-600 dark:text-amber-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg> </svg>
</div> </div>
</div> </div>
<p class="text-2xl font-bold text-gray-900 dark:text-white">${{ formatCost(stats.summary.avg_daily_cost) }}</p> <p class="text-2xl font-bold text-gray-900 dark:text-white">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ t('admin.accounts.stats.basedOnActualDays', { days: stats.summary.actual_days_used }) }}</p> ${{ formatCost(stats.summary.avg_daily_cost) }}
</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{
t('admin.accounts.stats.basedOnActualDays', {
days: stats.summary.actual_days_used
})
}}
</p>
</div> </div>
<!-- Daily Average Requests --> <!-- Daily Average Requests -->
<div class="card p-4 bg-gradient-to-br from-purple-50 to-white dark:from-purple-900/10 dark:to-dark-700 border-purple-200 dark:border-purple-800/30"> <div
<div class="flex items-center justify-between mb-2"> class="card border-purple-200 bg-gradient-to-br from-purple-50 to-white p-4 dark:border-purple-800/30 dark:from-purple-900/10 dark:to-dark-700"
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.avgDailyRequests') }}</span> >
<div class="p-1.5 rounded-lg bg-purple-100 dark:bg-purple-900/30"> <div class="mb-2 flex items-center justify-between">
<svg class="w-4 h-4 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" /> t('admin.accounts.stats.avgDailyRequests')
}}</span>
<div class="rounded-lg bg-purple-100 p-1.5 dark:bg-purple-900/30">
<svg
class="h-4 w-4 text-purple-600 dark:text-purple-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"
/>
</svg> </svg>
</div> </div>
</div> </div>
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ formatNumber(Math.round(stats.summary.avg_daily_requests)) }}</p> <p class="text-2xl font-bold text-gray-900 dark:text-white">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ t('admin.accounts.stats.avgDailyUsage') }}</p> {{ formatNumber(Math.round(stats.summary.avg_daily_requests)) }}
</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.stats.avgDailyUsage') }}
</p>
</div> </div>
</div> </div>
...@@ -105,78 +188,148 @@ ...@@ -105,78 +188,148 @@
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
<!-- Today Overview --> <!-- Today Overview -->
<div class="card p-4"> <div class="card p-4">
<div class="flex items-center gap-2 mb-3"> <div class="mb-3 flex items-center gap-2">
<div class="p-1.5 rounded-lg bg-cyan-100 dark:bg-cyan-900/30"> <div class="rounded-lg bg-cyan-100 p-1.5 dark:bg-cyan-900/30">
<svg class="w-4 h-4 text-cyan-600 dark:text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> class="h-4 w-4 text-cyan-600 dark:text-cyan-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.todayOverview') }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.todayOverview')
}}</span>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.cost') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">${{ formatCost(stats.summary.today?.cost || 0) }}</span> t('admin.accounts.stats.cost')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white"
>${{ formatCost(stats.summary.today?.cost || 0) }}</span
>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.requests') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatNumber(stats.summary.today?.requests || 0) }}</span> t('admin.accounts.stats.requests')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
formatNumber(stats.summary.today?.requests || 0)
}}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">Tokens</span> <span class="text-xs text-gray-500 dark:text-gray-400">Tokens</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatTokens(stats.summary.today?.tokens || 0) }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
formatTokens(stats.summary.today?.tokens || 0)
}}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Highest Cost Day --> <!-- Highest Cost Day -->
<div class="card p-4"> <div class="card p-4">
<div class="flex items-center gap-2 mb-3"> <div class="mb-3 flex items-center gap-2">
<div class="p-1.5 rounded-lg bg-orange-100 dark:bg-orange-900/30"> <div class="rounded-lg bg-orange-100 p-1.5 dark:bg-orange-900/30">
<svg class="w-4 h-4 text-orange-600 dark:text-orange-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z" /> class="h-4 w-4 text-orange-600 dark:text-orange-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z"
/>
</svg> </svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.highestCostDay') }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.highestCostDay')
}}</span>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.date') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ stats.summary.highest_cost_day?.label || '-' }}</span> t('admin.accounts.stats.date')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
stats.summary.highest_cost_day?.label || '-'
}}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.cost') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-orange-600 dark:text-orange-400">${{ formatCost(stats.summary.highest_cost_day?.cost || 0) }}</span> t('admin.accounts.stats.cost')
}}</span>
<span class="text-sm font-semibold text-orange-600 dark:text-orange-400"
>${{ formatCost(stats.summary.highest_cost_day?.cost || 0) }}</span
>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.requests') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatNumber(stats.summary.highest_cost_day?.requests || 0) }}</span> t('admin.accounts.stats.requests')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
formatNumber(stats.summary.highest_cost_day?.requests || 0)
}}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Highest Request Day --> <!-- Highest Request Day -->
<div class="card p-4"> <div class="card p-4">
<div class="flex items-center gap-2 mb-3"> <div class="mb-3 flex items-center gap-2">
<div class="p-1.5 rounded-lg bg-indigo-100 dark:bg-indigo-900/30"> <div class="rounded-lg bg-indigo-100 p-1.5 dark:bg-indigo-900/30">
<svg class="w-4 h-4 text-indigo-600 dark:text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /> class="h-4 w-4 text-indigo-600 dark:text-indigo-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg> </svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.highestRequestDay') }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.highestRequestDay')
}}</span>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.date') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ stats.summary.highest_request_day?.label || '-' }}</span> t('admin.accounts.stats.date')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
stats.summary.highest_request_day?.label || '-'
}}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.requests') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-indigo-600 dark:text-indigo-400">{{ formatNumber(stats.summary.highest_request_day?.requests || 0) }}</span> t('admin.accounts.stats.requests')
}}</span>
<span class="text-sm font-semibold text-indigo-600 dark:text-indigo-400">{{
formatNumber(stats.summary.highest_request_day?.requests || 0)
}}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.cost') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">${{ formatCost(stats.summary.highest_request_day?.cost || 0) }}</span> t('admin.accounts.stats.cost')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white"
>${{ formatCost(stats.summary.highest_request_day?.cost || 0) }}</span
>
</div> </div>
</div> </div>
</div> </div>
...@@ -186,70 +339,134 @@ ...@@ -186,70 +339,134 @@
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
<!-- Accumulated Tokens --> <!-- Accumulated Tokens -->
<div class="card p-4"> <div class="card p-4">
<div class="flex items-center gap-2 mb-3"> <div class="mb-3 flex items-center gap-2">
<div class="p-1.5 rounded-lg bg-teal-100 dark:bg-teal-900/30"> <div class="rounded-lg bg-teal-100 p-1.5 dark:bg-teal-900/30">
<svg class="w-4 h-4 text-teal-600 dark:text-teal-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /> class="h-4 w-4 text-teal-600 dark:text-teal-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
/>
</svg> </svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.accumulatedTokens') }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.accumulatedTokens')
}}</span>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.totalTokens') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatTokens(stats.summary.total_tokens) }}</span> t('admin.accounts.stats.totalTokens')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
formatTokens(stats.summary.total_tokens)
}}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.dailyAvgTokens') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatTokens(Math.round(stats.summary.avg_daily_tokens)) }}</span> t('admin.accounts.stats.dailyAvgTokens')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
formatTokens(Math.round(stats.summary.avg_daily_tokens))
}}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Performance --> <!-- Performance -->
<div class="card p-4"> <div class="card p-4">
<div class="flex items-center gap-2 mb-3"> <div class="mb-3 flex items-center gap-2">
<div class="p-1.5 rounded-lg bg-rose-100 dark:bg-rose-900/30"> <div class="rounded-lg bg-rose-100 p-1.5 dark:bg-rose-900/30">
<svg class="w-4 h-4 text-rose-600 dark:text-rose-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> class="h-4 w-4 text-rose-600 dark:text-rose-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg> </svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.performance') }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.performance')
}}</span>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.avgResponseTime') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatDuration(stats.summary.avg_duration_ms) }}</span> t('admin.accounts.stats.avgResponseTime')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
formatDuration(stats.summary.avg_duration_ms)
}}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.daysActive') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ stats.summary.actual_days_used }} / {{ stats.summary.days }}</span> t('admin.accounts.stats.daysActive')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white"
>{{ stats.summary.actual_days_used }} / {{ stats.summary.days }}</span
>
</div> </div>
</div> </div>
</div> </div>
<!-- Recent Activity --> <!-- Recent Activity -->
<div class="card p-4"> <div class="card p-4">
<div class="flex items-center gap-2 mb-3"> <div class="mb-3 flex items-center gap-2">
<div class="p-1.5 rounded-lg bg-lime-100 dark:bg-lime-900/30"> <div class="rounded-lg bg-lime-100 p-1.5 dark:bg-lime-900/30">
<svg class="w-4 h-4 text-lime-600 dark:text-lime-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> class="h-4 w-4 text-lime-600 dark:text-lime-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg> </svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.recentActivity') }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.recentActivity')
}}</span>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.todayRequests') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatNumber(stats.summary.today?.requests || 0) }}</span> t('admin.accounts.stats.todayRequests')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
formatNumber(stats.summary.today?.requests || 0)
}}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.todayTokens') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatTokens(stats.summary.today?.tokens || 0) }}</span> t('admin.accounts.stats.todayTokens')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
formatTokens(stats.summary.today?.tokens || 0)
}}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.todayCost') }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{
<span class="text-sm font-semibold text-gray-900 dark:text-white">${{ formatCost(stats.summary.today?.cost || 0) }}</span> t('admin.accounts.stats.todayCost')
}}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white"
>${{ formatCost(stats.summary.today?.cost || 0) }}</span
>
</div> </div>
</div> </div>
</div> </div>
...@@ -257,26 +474,36 @@ ...@@ -257,26 +474,36 @@
<!-- Usage Trend Chart --> <!-- Usage Trend Chart -->
<div class="card p-4"> <div class="card p-4">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-4">{{ t('admin.accounts.stats.usageTrend') }}</h3> <h3 class="mb-4 text-sm font-semibold text-gray-900 dark:text-white">
{{ t('admin.accounts.stats.usageTrend') }}
</h3>
<div class="h-64"> <div class="h-64">
<Line v-if="trendChartData" :data="trendChartData" :options="lineChartOptions" /> <Line v-if="trendChartData" :data="trendChartData" :options="lineChartOptions" />
<div v-else class="flex items-center justify-center h-full text-gray-500 dark:text-gray-400 text-sm"> <div
v-else
class="flex h-full items-center justify-center text-sm text-gray-500 dark:text-gray-400"
>
{{ t('admin.dashboard.noDataAvailable') }} {{ t('admin.dashboard.noDataAvailable') }}
</div> </div>
</div> </div>
</div> </div>
<!-- Model Distribution --> <!-- Model Distribution -->
<ModelDistributionChart <ModelDistributionChart :model-stats="stats.models" :loading="false" />
:model-stats="stats.models"
:loading="false"
/>
</template> </template>
<!-- No Data State --> <!-- No Data State -->
<div v-else-if="!loading" class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"> <div
<svg class="w-12 h-12 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> v-else-if="!loading"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
>
<svg class="mb-4 h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg> </svg>
<p class="text-sm">{{ t('admin.accounts.stats.noData') }}</p> <p class="text-sm">{{ t('admin.accounts.stats.noData') }}</p>
</div> </div>
...@@ -286,7 +513,7 @@ ...@@ -286,7 +513,7 @@
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
@click="handleClose" @click="handleClose"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-dark-600 hover:bg-gray-200 dark:hover:bg-dark-500 rounded-lg transition-colors" class="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-dark-500"
> >
{{ t('common.close') }} {{ t('common.close') }}
</button> </button>
...@@ -349,7 +576,7 @@ const isDarkMode = computed(() => { ...@@ -349,7 +576,7 @@ const isDarkMode = computed(() => {
// Chart colors // Chart colors
const chartColors = computed(() => ({ const chartColors = computed(() => ({
text: isDarkMode.value ? '#e5e7eb' : '#374151', text: isDarkMode.value ? '#e5e7eb' : '#374151',
grid: isDarkMode.value ? '#374151' : '#e5e7eb', grid: isDarkMode.value ? '#374151' : '#e5e7eb'
})) }))
// Line chart data // Line chart data
...@@ -357,27 +584,27 @@ const trendChartData = computed(() => { ...@@ -357,27 +584,27 @@ const trendChartData = computed(() => {
if (!stats.value?.history?.length) return null if (!stats.value?.history?.length) return null
return { return {
labels: stats.value.history.map(h => h.label), labels: stats.value.history.map((h) => h.label),
datasets: [ datasets: [
{ {
label: t('admin.accounts.stats.cost') + ' (USD)', label: t('admin.accounts.stats.cost') + ' (USD)',
data: stats.value.history.map(h => h.cost), data: stats.value.history.map((h) => h.cost),
borderColor: '#3b82f6', borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)', backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true, fill: true,
tension: 0.3, tension: 0.3,
yAxisID: 'y', yAxisID: 'y'
}, },
{ {
label: t('admin.accounts.stats.requests'), label: t('admin.accounts.stats.requests'),
data: stats.value.history.map(h => h.requests), data: stats.value.history.map((h) => h.requests),
borderColor: '#f97316', borderColor: '#f97316',
backgroundColor: 'rgba(249, 115, 22, 0.1)', backgroundColor: 'rgba(249, 115, 22, 0.1)',
fill: false, fill: false,
tension: 0.3, tension: 0.3,
yAxisID: 'y1', yAxisID: 'y1'
}, }
], ]
} }
}) })
...@@ -387,7 +614,7 @@ const lineChartOptions = computed(() => ({ ...@@ -387,7 +614,7 @@ const lineChartOptions = computed(() => ({
maintainAspectRatio: false, maintainAspectRatio: false,
interaction: { interaction: {
intersect: false, intersect: false,
mode: 'index' as const, mode: 'index' as const
}, },
plugins: { plugins: {
legend: { legend: {
...@@ -398,9 +625,9 @@ const lineChartOptions = computed(() => ({ ...@@ -398,9 +625,9 @@ const lineChartOptions = computed(() => ({
pointStyle: 'circle', pointStyle: 'circle',
padding: 15, padding: 15,
font: { font: {
size: 11, size: 11
}, }
}, }
}, },
tooltip: { tooltip: {
callbacks: { callbacks: {
...@@ -411,81 +638,84 @@ const lineChartOptions = computed(() => ({ ...@@ -411,81 +638,84 @@ const lineChartOptions = computed(() => ({
return `${label}: $${formatCost(value)}` return `${label}: $${formatCost(value)}`
} }
return `${label}: ${formatNumber(value)}` return `${label}: ${formatNumber(value)}`
}, }
}, }
}, }
}, },
scales: { scales: {
x: { x: {
grid: { grid: {
color: chartColors.value.grid, color: chartColors.value.grid
}, },
ticks: { ticks: {
color: chartColors.value.text, color: chartColors.value.text,
font: { font: {
size: 10, size: 10
}, },
maxRotation: 45, maxRotation: 45,
minRotation: 0, minRotation: 0
}, }
}, },
y: { y: {
type: 'linear' as const, type: 'linear' as const,
display: true, display: true,
position: 'left' as const, position: 'left' as const,
grid: { grid: {
color: chartColors.value.grid, color: chartColors.value.grid
}, },
ticks: { ticks: {
color: '#3b82f6', color: '#3b82f6',
font: { font: {
size: 10, size: 10
}, },
callback: (value: string | number) => '$' + formatCost(Number(value)), callback: (value: string | number) => '$' + formatCost(Number(value))
}, },
title: { title: {
display: true, display: true,
text: t('admin.accounts.stats.cost') + ' (USD)', text: t('admin.accounts.stats.cost') + ' (USD)',
color: '#3b82f6', color: '#3b82f6',
font: { font: {
size: 11, size: 11
}, }
}, }
}, },
y1: { y1: {
type: 'linear' as const, type: 'linear' as const,
display: true, display: true,
position: 'right' as const, position: 'right' as const,
grid: { grid: {
drawOnChartArea: false, drawOnChartArea: false
}, },
ticks: { ticks: {
color: '#f97316', color: '#f97316',
font: { font: {
size: 10, size: 10
}, },
callback: (value: string | number) => formatNumber(Number(value)), callback: (value: string | number) => formatNumber(Number(value))
}, },
title: { title: {
display: true, display: true,
text: t('admin.accounts.stats.requests'), text: t('admin.accounts.stats.requests'),
color: '#f97316', color: '#f97316',
font: { font: {
size: 11, size: 11
}, }
}, }
}, }
}, }
})) }))
// Load stats when modal opens // Load stats when modal opens
watch(() => props.show, async (newVal) => { watch(
if (newVal && props.account) { () => props.show,
await loadStats() async (newVal) => {
} else { if (newVal && props.account) {
stats.value = null await loadStats()
} else {
stats.value = null
}
} }
}) )
const loadStats = async () => { const loadStats = async () => {
if (!props.account) return if (!props.account) return
......
<template> <template>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Main Status Badge --> <!-- Main Status Badge -->
<span <span :class="['badge text-xs', statusClass]">
:class="[
'badge text-xs',
statusClass
]"
>
{{ statusText }} {{ statusText }}
</span> </span>
<!-- Error Info Indicator --> <!-- Error Info Indicator -->
<div v-if="hasError && account.error_message" class="relative group/error"> <div v-if="hasError && account.error_message" class="group/error relative">
<svg class="w-4 h-4 text-red-500 dark:text-red-400 cursor-help hover:text-red-600 dark:hover:text-red-300 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" /> class="h-4 w-4 cursor-help text-red-500 transition-colors hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
/>
</svg> </svg>
<!-- Tooltip - 向下显示 --> <!-- Tooltip - 向下显示 -->
<div class="absolute top-full left-0 mt-1.5 px-3 py-2 bg-gray-800 dark:bg-gray-900 text-white text-xs rounded-lg shadow-xl opacity-0 invisible group-hover/error:opacity-100 group-hover/error:visible transition-all duration-200 z-[100] min-w-[200px] max-w-[300px]"> <div
<div class="text-gray-300 break-words whitespace-pre-wrap leading-relaxed">{{ account.error_message }}</div> class="invisible absolute left-0 top-full z-[100] mt-1.5 min-w-[200px] max-w-[300px] rounded-lg bg-gray-800 px-3 py-2 text-xs text-white opacity-0 shadow-xl transition-all duration-200 group-hover/error:visible group-hover/error:opacity-100 dark:bg-gray-900"
>
<div class="whitespace-pre-wrap break-words leading-relaxed text-gray-300">
{{ account.error_message }}
</div>
<!-- 上方小三角 --> <!-- 上方小三角 -->
<div class="absolute bottom-full left-3 border-[6px] border-transparent border-b-gray-800 dark:border-b-gray-900"></div> <div
class="absolute bottom-full left-3 border-[6px] border-transparent border-b-gray-800 dark:border-b-gray-900"
></div>
</div> </div>
</div> </div>
<!-- Rate Limit Indicator (429) --> <!-- Rate Limit Indicator (429) -->
<div v-if="isRateLimited" class="relative group"> <div v-if="isRateLimited" class="group relative">
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"> <span
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> class="inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> >
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg> </svg>
429 429
</span> </span>
<!-- Tooltip --> <!-- Tooltip -->
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50"> <div
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
>
Rate limited until {{ formatTime(account.rate_limit_reset_at) }} Rate limited until {{ formatTime(account.rate_limit_reset_at) }}
<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div> <div
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
></div>
</div> </div>
</div> </div>
<!-- Overload Indicator (529) --> <!-- Overload Indicator (529) -->
<div v-if="isOverloaded" class="relative group"> <div v-if="isOverloaded" class="group relative">
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"> <span
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> class="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> >
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg> </svg>
529 529
</span> </span>
<!-- Tooltip --> <!-- Tooltip -->
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50"> <div
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
>
Overloaded until {{ formatTime(account.overload_until) }} Overloaded until {{ formatTime(account.overload_until) }}
<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div> <div
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
></div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -7,17 +7,29 @@ ...@@ -7,17 +7,29 @@
> >
<div class="space-y-4"> <div class="space-y-4">
<!-- Account Info Card --> <!-- Account Info Card -->
<div v-if="account" class="flex items-center justify-between p-3 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-dark-700 dark:to-dark-600 rounded-xl border border-gray-200 dark:border-dark-500"> <div
v-if="account"
class="flex items-center justify-between rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-gray-100 p-3 dark:border-dark-500 dark:from-dark-700 dark:to-dark-600"
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center"> <div
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> >
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
</div> </div>
<div> <div>
<div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div> <div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1.5"> <div class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
<span class="px-1.5 py-0.5 bg-gray-200 dark:bg-dark-500 rounded text-[10px] font-medium uppercase"> <span
class="rounded bg-gray-200 px-1.5 py-0.5 text-[10px] font-medium uppercase dark:bg-dark-500"
>
{{ account.type }} {{ account.type }}
</span> </span>
<span>{{ t('admin.accounts.account') }}</span> <span>{{ t('admin.accounts.account') }}</span>
...@@ -26,7 +38,7 @@ ...@@ -26,7 +38,7 @@
</div> </div>
<span <span
:class="[ :class="[
'px-2.5 py-1 text-xs font-semibold rounded-full', 'rounded-full px-2.5 py-1 text-xs font-semibold',
account.status === 'active' account.status === 'active'
? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400' ? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400'
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400' : 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'
...@@ -44,7 +56,7 @@ ...@@ -44,7 +56,7 @@
<select <select
v-model="selectedModelId" v-model="selectedModelId"
:disabled="loadingModels || status === 'connecting'" :disabled="loadingModels || status === 'connecting'"
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-dark-500 bg-white dark:bg-dark-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50 disabled:cursor-not-allowed" class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-primary-500 focus:ring-2 focus:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-100"
> >
<option v-if="loadingModels" value="">{{ t('common.loading') }}...</option> <option v-if="loadingModels" value="">{{ t('common.loading') }}...</option>
<option v-for="model in availableModels" :key="model.id" :value="model.id"> <option v-for="model in availableModels" :key="model.id" :value="model.id">
...@@ -54,22 +66,38 @@ ...@@ -54,22 +66,38 @@
</div> </div>
<!-- Terminal Output --> <!-- Terminal Output -->
<div class="relative group"> <div class="group relative">
<div <div
ref="terminalRef" ref="terminalRef"
class="bg-gray-900 dark:bg-black rounded-xl p-4 min-h-[120px] max-h-[240px] overflow-y-auto font-mono text-sm border border-gray-700 dark:border-gray-800" class="max-h-[240px] min-h-[120px] overflow-y-auto rounded-xl border border-gray-700 bg-gray-900 p-4 font-mono text-sm dark:border-gray-800 dark:bg-black"
> >
<!-- Status Line --> <!-- Status Line -->
<div v-if="status === 'idle'" class="text-gray-500 flex items-center gap-2"> <div v-if="status === 'idle'" class="flex items-center gap-2 text-gray-500">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg> </svg>
<span>{{ t('admin.accounts.readyToTest') }}</span> <span>{{ t('admin.accounts.readyToTest') }}</span>
</div> </div>
<div v-else-if="status === 'connecting'" class="text-yellow-400 flex items-center gap-2"> <div v-else-if="status === 'connecting'" class="flex items-center gap-2 text-yellow-400">
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24"> <svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
<span>{{ t('admin.accounts.connectingToApi') }}</span> <span>{{ t('admin.accounts.connectingToApi') }}</span>
</div> </div>
...@@ -85,15 +113,31 @@ ...@@ -85,15 +113,31 @@
</div> </div>
<!-- Result Status --> <!-- Result Status -->
<div v-if="status === 'success'" class="text-green-400 mt-3 pt-3 border-t border-gray-700 flex items-center gap-2"> <div
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> v-if="status === 'success'"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-green-400"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
<span>{{ t('admin.accounts.testCompleted') }}</span> <span>{{ t('admin.accounts.testCompleted') }}</span>
</div> </div>
<div v-else-if="status === 'error'" class="text-red-400 mt-3 pt-3 border-t border-gray-700 flex items-center gap-2"> <div
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> v-else-if="status === 'error'"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-red-400"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
<span>{{ errorMessage }}</span> <span>{{ errorMessage }}</span>
</div> </div>
...@@ -103,28 +147,43 @@ ...@@ -103,28 +147,43 @@
<button <button
v-if="outputLines.length > 0" v-if="outputLines.length > 0"
@click="copyOutput" @click="copyOutput"
class="absolute top-2 right-2 p-1.5 text-gray-400 hover:text-white bg-gray-800/80 hover:bg-gray-700 rounded-lg transition-all opacity-0 group-hover:opacity-100" class="absolute right-2 top-2 rounded-lg bg-gray-800/80 p-1.5 text-gray-400 opacity-0 transition-all hover:bg-gray-700 hover:text-white group-hover:opacity-100"
:title="t('admin.accounts.copyOutput')" :title="t('admin.accounts.copyOutput')"
> >
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg> </svg>
</button> </button>
</div> </div>
<!-- Test Info --> <!-- Test Info -->
<div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 px-1"> <div class="flex items-center justify-between px-1 text-xs text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
/>
</svg> </svg>
{{ t('admin.accounts.testModel') }} {{ t('admin.accounts.testModel') }}
</span> </span>
</div> </div>
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg> </svg>
{{ t('admin.accounts.testPrompt') }} {{ t('admin.accounts.testPrompt') }}
</span> </span>
...@@ -135,7 +194,7 @@ ...@@ -135,7 +194,7 @@
<div class="flex justify-end gap-3"> <div class="flex justify-end gap-3">
<button <button
@click="handleClose" @click="handleClose"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-dark-600 hover:bg-gray-200 dark:hover:bg-dark-500 rounded-lg transition-colors" class="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-dark-500"
:disabled="status === 'connecting'" :disabled="status === 'connecting'"
> >
{{ t('common.close') }} {{ t('common.close') }}
...@@ -144,29 +203,72 @@ ...@@ -144,29 +203,72 @@
@click="startTest" @click="startTest"
:disabled="status === 'connecting' || !selectedModelId" :disabled="status === 'connecting' || !selectedModelId"
:class="[ :class="[
'px-4 py-2 text-sm font-medium rounded-lg transition-all flex items-center gap-2', 'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
status === 'connecting' || !selectedModelId status === 'connecting' || !selectedModelId
? 'bg-primary-400 text-white cursor-not-allowed' ? 'cursor-not-allowed bg-primary-400 text-white'
: status === 'success' : status === 'success'
? 'bg-green-500 hover:bg-green-600 text-white' ? 'bg-green-500 text-white hover:bg-green-600'
: status === 'error' : status === 'error'
? 'bg-orange-500 hover:bg-orange-600 text-white' ? 'bg-orange-500 text-white hover:bg-orange-600'
: 'bg-primary-500 hover:bg-primary-600 text-white' : 'bg-primary-500 text-white hover:bg-primary-600'
]" ]"
> >
<svg v-if="status === 'connecting'" class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24"> <svg
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> v-if="status === 'connecting'"
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> class="h-4 w-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
<svg v-else-if="status === 'idle'" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /> v-else-if="status === 'idle'"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
<svg v-else class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg v-else class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg> </svg>
<span> <span>
{{ status === 'connecting' ? t('admin.accounts.testing') : status === 'idle' ? t('admin.accounts.startTest') : t('admin.accounts.retry') }} {{
status === 'connecting'
? t('admin.accounts.testing')
: status === 'idle'
? t('admin.accounts.startTest')
: t('admin.accounts.retry')
}}
</span> </span>
</button> </button>
</div> </div>
...@@ -208,14 +310,17 @@ const loadingModels = ref(false) ...@@ -208,14 +310,17 @@ const loadingModels = ref(false)
let eventSource: EventSource | null = null let eventSource: EventSource | null = null
// Load available models when modal opens // Load available models when modal opens
watch(() => props.show, async (newVal) => { watch(
if (newVal && props.account) { () => props.show,
resetState() async (newVal) => {
await loadAvailableModels() if (newVal && props.account) {
} else { resetState()
closeEventSource() await loadAvailableModels()
} else {
closeEventSource()
}
} }
}) )
const loadAvailableModels = async () => { const loadAvailableModels = async () => {
if (!props.account) return if (!props.account) return
...@@ -224,11 +329,18 @@ const loadAvailableModels = async () => { ...@@ -224,11 +329,18 @@ const loadAvailableModels = async () => {
selectedModelId.value = '' // Reset selection before loading selectedModelId.value = '' // Reset selection before loading
try { try {
availableModels.value = await adminAPI.accounts.getAvailableModels(props.account.id) availableModels.value = await adminAPI.accounts.getAvailableModels(props.account.id)
// Default to first model (usually Sonnet) // Default selection by platform
if (availableModels.value.length > 0) { if (availableModels.value.length > 0) {
// Try to select Sonnet as default, otherwise use first model if (props.account.platform === 'gemini') {
const sonnetModel = availableModels.value.find(m => m.id.includes('sonnet')) const preferred =
selectedModelId.value = sonnetModel?.id || availableModels.value[0].id availableModels.value.find((m) => m.id === 'gemini-2.5-pro') ||
availableModels.value.find((m) => m.id === 'gemini-3-pro')
selectedModelId.value = preferred?.id || availableModels.value[0].id
} else {
// Try to select Sonnet as default, otherwise use first model
const sonnetModel = availableModels.value.find((m) => m.id.includes('sonnet'))
selectedModelId.value = sonnetModel?.id || availableModels.value[0].id
}
} }
} catch (error) { } catch (error) {
console.error('Failed to load available models:', error) console.error('Failed to load available models:', error)
...@@ -290,7 +402,7 @@ const startTest = async () => { ...@@ -290,7 +402,7 @@ const startTest = async () => {
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ model_id: selectedModelId.value }) body: JSON.stringify({ model_id: selectedModelId.value })
...@@ -337,7 +449,13 @@ const startTest = async () => { ...@@ -337,7 +449,13 @@ const startTest = async () => {
} }
} }
const handleEvent = (event: { type: string; text?: string; model?: string; success?: boolean; error?: string }) => { const handleEvent = (event: {
type: string
text?: string
model?: string
success?: boolean
error?: string
}) => {
switch (event.type) { switch (event.type) {
case 'test_start': case 'test_start':
addLine(t('admin.accounts.connectedToApi'), 'text-green-400') addLine(t('admin.accounts.connectedToApi'), 'text-green-400')
...@@ -382,7 +500,7 @@ const handleEvent = (event: { type: string; text?: string; model?: string; succe ...@@ -382,7 +500,7 @@ const handleEvent = (event: { type: string; text?: string; model?: string; succe
} }
const copyOutput = () => { const copyOutput = () => {
const text = outputLines.value.map(l => l.text).join('\n') const text = outputLines.value.map((l) => l.text).join('\n')
navigator.clipboard.writeText(text) navigator.clipboard.writeText(text)
} }
</script> </script>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment