feat(copilot): finish configuration center
This commit is contained in:
@@ -1,74 +1,144 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import CaptainPreferencesAPI from 'dashboard/api/captain/preferences';
|
||||
import CopilotConfigAPI from 'dashboard/api/copilotConfig';
|
||||
|
||||
const applyAccountPayload = (store, payload = {}) => {
|
||||
store.providers = payload.providers || {};
|
||||
store.models = payload.models || {};
|
||||
store.features = payload.features || {};
|
||||
store.behavior = payload.behavior || {};
|
||||
store.providerConfig = payload.provider_config || {};
|
||||
};
|
||||
|
||||
export const useCaptainConfigStore = defineStore('captainConfig', {
|
||||
state: () => ({
|
||||
providers: {},
|
||||
models: {},
|
||||
features: {},
|
||||
providerConfig: {},
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
},
|
||||
}),
|
||||
state: () => ({
|
||||
providers: {},
|
||||
models: {},
|
||||
features: {},
|
||||
behavior: {},
|
||||
providerConfig: {},
|
||||
providerTestResult: null,
|
||||
embeddingReindexStatus: {},
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
isSavingAccount: false,
|
||||
isSavingProvider: false,
|
||||
isTestingProvider: false,
|
||||
isStartingReindex: false,
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getProviders: state => state.providers,
|
||||
getModels: state => state.models,
|
||||
getFeatures: state => state.features,
|
||||
getUIFlags: state => state.uiFlags,
|
||||
getModelsForFeature: state => featureKey => {
|
||||
const feature = state.features[featureKey];
|
||||
const models = feature?.models || [];
|
||||
getters: {
|
||||
getProviders: state => state.providers,
|
||||
getModels: state => state.models,
|
||||
getFeatures: state => state.features,
|
||||
getUIFlags: state => state.uiFlags,
|
||||
getModelsForFeature: state => featureKey => {
|
||||
const feature = state.features[featureKey];
|
||||
const models = feature?.models || [];
|
||||
|
||||
const providerOrder = { openai: 0, anthropic: 1, gemini: 2 };
|
||||
const providerOrder = { openai: 0, anthropic: 1, gemini: 2 };
|
||||
|
||||
return [...models].sort((a, b) => {
|
||||
// Move coming_soon items to the end
|
||||
if (a.coming_soon && !b.coming_soon) return 1;
|
||||
if (!a.coming_soon && b.coming_soon) return -1;
|
||||
return [...models].sort((a, b) => {
|
||||
// Move coming_soon items to the end
|
||||
if (a.coming_soon && !b.coming_soon) return 1;
|
||||
if (!a.coming_soon && b.coming_soon) return -1;
|
||||
|
||||
// Sort by provider
|
||||
const providerA = providerOrder[a.provider] ?? 999;
|
||||
const providerB = providerOrder[b.provider] ?? 999;
|
||||
if (providerA !== providerB) return providerA - providerB;
|
||||
// Sort by provider
|
||||
const providerA = providerOrder[a.provider] ?? 999;
|
||||
const providerB = providerOrder[b.provider] ?? 999;
|
||||
if (providerA !== providerB) return providerA - providerB;
|
||||
|
||||
// Sort by credit_multiplier (highest first)
|
||||
return (b.credit_multiplier || 0) - (a.credit_multiplier || 0);
|
||||
});
|
||||
},
|
||||
getDefaultModelForFeature: state => featureKey => {
|
||||
const feature = state.features[featureKey];
|
||||
return feature?.default || null;
|
||||
},
|
||||
getSelectedModelForFeature: state => featureKey => {
|
||||
const feature = state.features[featureKey];
|
||||
return feature?.selected || feature?.default || null;
|
||||
},
|
||||
},
|
||||
// Sort by credit_multiplier (highest first)
|
||||
return (b.credit_multiplier || 0) - (a.credit_multiplier || 0);
|
||||
});
|
||||
},
|
||||
getDefaultModelForFeature: state => featureKey => {
|
||||
const feature = state.features[featureKey];
|
||||
return feature?.default || null;
|
||||
},
|
||||
getSelectedModelForFeature: state => featureKey => {
|
||||
const feature = state.features[featureKey];
|
||||
return feature?.selected || feature?.default || null;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetch() {
|
||||
this.uiFlags.isFetching = true;
|
||||
try {
|
||||
const response = await CaptainPreferencesAPI.get();
|
||||
this.providers = response.data.providers || {};
|
||||
this.models = response.data.models || {};
|
||||
this.features = response.data.features || {};
|
||||
this.providerConfig = response.data.provider_config || {};
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
} finally {
|
||||
this.uiFlags.isFetching = false;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async fetch({ includePlatform = false } = {}) {
|
||||
this.uiFlags.isFetching = true;
|
||||
try {
|
||||
const accountResponse =
|
||||
await CopilotConfigAPI.getAccountConfig();
|
||||
applyAccountPayload(this, accountResponse.data);
|
||||
if (includePlatform) {
|
||||
const [platformResponse, reindexResponse] =
|
||||
await Promise.all([
|
||||
CopilotConfigAPI.getPlatformConfig(),
|
||||
CopilotConfigAPI.getEmbeddingReindexStatus(),
|
||||
]);
|
||||
this.providerConfig = platformResponse.data || {};
|
||||
this.embeddingReindexStatus = reindexResponse.data || {};
|
||||
}
|
||||
} finally {
|
||||
this.uiFlags.isFetching = false;
|
||||
}
|
||||
},
|
||||
|
||||
async updatePreferences(data) {
|
||||
const response = await CaptainPreferencesAPI.updatePreferences(data);
|
||||
this.providers = response.data.providers || {};
|
||||
this.models = response.data.models || {};
|
||||
this.features = response.data.features || {};
|
||||
this.providerConfig = response.data.provider_config || {};
|
||||
},
|
||||
},
|
||||
async updateAccountConfig(data) {
|
||||
this.uiFlags.isSavingAccount = true;
|
||||
try {
|
||||
const response =
|
||||
await CopilotConfigAPI.updateAccountConfig(data);
|
||||
applyAccountPayload(this, response.data);
|
||||
return response.data;
|
||||
} finally {
|
||||
this.uiFlags.isSavingAccount = false;
|
||||
}
|
||||
},
|
||||
|
||||
async updatePreferences(data) {
|
||||
return this.updateAccountConfig(data);
|
||||
},
|
||||
|
||||
async updatePlatformConfig(data) {
|
||||
this.uiFlags.isSavingProvider = true;
|
||||
try {
|
||||
const response =
|
||||
await CopilotConfigAPI.updatePlatformConfig(data);
|
||||
this.providerConfig = response.data || {};
|
||||
this.providerTestResult = null;
|
||||
return response.data;
|
||||
} finally {
|
||||
this.uiFlags.isSavingProvider = false;
|
||||
}
|
||||
},
|
||||
|
||||
async testPlatformConfig(data) {
|
||||
this.uiFlags.isTestingProvider = true;
|
||||
try {
|
||||
const response =
|
||||
await CopilotConfigAPI.testPlatformConfig(data);
|
||||
this.providerTestResult = response.data || null;
|
||||
return response.data;
|
||||
} finally {
|
||||
this.uiFlags.isTestingProvider = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchEmbeddingReindexStatus() {
|
||||
const response = await CopilotConfigAPI.getEmbeddingReindexStatus();
|
||||
this.embeddingReindexStatus = response.data || {};
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async startEmbeddingReindex() {
|
||||
this.uiFlags.isStartingReindex = true;
|
||||
try {
|
||||
const response = await CopilotConfigAPI.startEmbeddingReindex();
|
||||
this.embeddingReindexStatus = response.data || {};
|
||||
return response.data;
|
||||
} finally {
|
||||
this.uiFlags.isStartingReindex = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import CopilotConfigAPI from 'dashboard/api/copilotConfig';
|
||||
import { useCaptainConfigStore } from './preferences';
|
||||
|
||||
vi.mock('dashboard/api/copilotConfig', () => ({
|
||||
default: {
|
||||
getAccountConfig: vi.fn(),
|
||||
updateAccountConfig: vi.fn(),
|
||||
getPlatformConfig: vi.fn(),
|
||||
updatePlatformConfig: vi.fn(),
|
||||
testPlatformConfig: vi.fn(),
|
||||
getEmbeddingReindexStatus: vi.fn(),
|
||||
startEmbeddingReindex: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const accountPayload = {
|
||||
providers: { openai: { display_name: 'OpenAI' } },
|
||||
models: { 'account-model': { provider: 'openai' } },
|
||||
features: {
|
||||
editor: {
|
||||
default: 'platform-model',
|
||||
selected: 'account-model',
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
behavior: { tone: 'friendly' },
|
||||
provider_config: { configured: true, chat: { model: 'account-view' } },
|
||||
};
|
||||
|
||||
describe('Copilot configuration store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('loads account config and replaces provider details for SuperAdmin', async () => {
|
||||
CopilotConfigAPI.getAccountConfig.mockResolvedValue({
|
||||
data: accountPayload,
|
||||
});
|
||||
CopilotConfigAPI.getPlatformConfig.mockResolvedValue({
|
||||
data: { configured: true, chat: { model: 'platform-model' } },
|
||||
});
|
||||
CopilotConfigAPI.getEmbeddingReindexStatus.mockResolvedValue({
|
||||
data: { running: false, processed: 3, total: 3 },
|
||||
});
|
||||
const store = useCaptainConfigStore();
|
||||
|
||||
await store.fetch({ includePlatform: true });
|
||||
|
||||
expect(store.behavior.tone).toBe('friendly');
|
||||
expect(store.features.editor.selected).toBe('account-model');
|
||||
expect(store.providerConfig.chat.model).toBe('platform-model');
|
||||
expect(CopilotConfigAPI.getPlatformConfig).toHaveBeenCalledOnce();
|
||||
expect(store.embeddingReindexStatus.processed).toBe(3);
|
||||
expect(store.uiFlags.isFetching).toBe(false);
|
||||
});
|
||||
|
||||
it('does not request platform secrets for an account administrator', async () => {
|
||||
CopilotConfigAPI.getAccountConfig.mockResolvedValue({
|
||||
data: accountPayload,
|
||||
});
|
||||
const store = useCaptainConfigStore();
|
||||
|
||||
await store.fetch();
|
||||
|
||||
expect(CopilotConfigAPI.getPlatformConfig).not.toHaveBeenCalled();
|
||||
expect(store.providerConfig.chat.model).toBe('account-view');
|
||||
});
|
||||
|
||||
it('uses separate account, platform save, and provider test endpoints', async () => {
|
||||
CopilotConfigAPI.updateAccountConfig.mockResolvedValue({
|
||||
data: { ...accountPayload, behavior: { tone: 'formal' } },
|
||||
});
|
||||
CopilotConfigAPI.updatePlatformConfig.mockResolvedValue({
|
||||
data: { configured: true, chat: { model: 'saved-model' } },
|
||||
});
|
||||
CopilotConfigAPI.testPlatformConfig.mockResolvedValue({
|
||||
data: { chat: { ok: true }, embedding: { ok: true } },
|
||||
});
|
||||
CopilotConfigAPI.startEmbeddingReindex.mockResolvedValue({
|
||||
data: { running: true, processed: 0, total: 2 },
|
||||
});
|
||||
const store = useCaptainConfigStore();
|
||||
|
||||
await store.updateAccountConfig({ behavior: { tone: 'formal' } });
|
||||
await store.updatePlatformConfig({ chat: { model: 'saved-model' } });
|
||||
await store.testPlatformConfig({ chat: { model: 'candidate-model' } });
|
||||
await store.startEmbeddingReindex();
|
||||
|
||||
expect(store.behavior.tone).toBe('formal');
|
||||
expect(store.providerConfig.chat.model).toBe('saved-model');
|
||||
expect(store.providerTestResult.embedding.ok).toBe(true);
|
||||
expect(CopilotConfigAPI.updateAccountConfig).toHaveBeenCalledOnce();
|
||||
expect(CopilotConfigAPI.updatePlatformConfig).toHaveBeenCalledOnce();
|
||||
expect(CopilotConfigAPI.testPlatformConfig).toHaveBeenCalledOnce();
|
||||
expect(CopilotConfigAPI.startEmbeddingReindex).toHaveBeenCalledOnce();
|
||||
expect(store.embeddingReindexStatus.running).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user