feat(copilot): move provider configuration to settings
This commit is contained in:
@@ -645,12 +645,12 @@ const menuItems = computed(() => {
|
||||
icon: 'i-lucide-briefcase',
|
||||
to: accountScopedRoute('general_settings_index'),
|
||||
},
|
||||
// {
|
||||
// name: 'Settings Captain',
|
||||
// label: t('SIDEBAR.CAPTAIN_AI'),
|
||||
// icon: 'i-woot-captain',
|
||||
// to: accountScopedRoute('captain_settings_index'),
|
||||
// },
|
||||
{
|
||||
name: 'Settings Captain',
|
||||
label: t('SIDEBAR.CAPTAIN_AI'),
|
||||
icon: 'i-woot-captain',
|
||||
to: accountScopedRoute('captain_settings_index'),
|
||||
},
|
||||
{
|
||||
name: 'Settings Agents',
|
||||
label: t('SIDEBAR.AGENTS'),
|
||||
|
||||
@@ -382,15 +382,28 @@
|
||||
},
|
||||
"DOCS": "Read docs",
|
||||
"SECURITY": "Security",
|
||||
"CAPTAIN_AI": "Captain",
|
||||
"CAPTAIN_AI": "Copilot Configuration",
|
||||
"CONVERSATION_WORKFLOW": "Conversation Workflow"
|
||||
},
|
||||
"CAPTAIN_SETTINGS": {
|
||||
"TITLE": "Captain Settings",
|
||||
"DESCRIPTION": "Configure your AI models and features for Captain. Captain follows a credit based billing, you will be charged credits for every action Captain takes based on the model selected.",
|
||||
"TITLE": "Copilot Configuration",
|
||||
"DESCRIPTION": "Configure the AI provider and models used by Copilot. Configuration is stored securely and takes effect without restarting the service.",
|
||||
"LOADING": "Loading Captain configuration...",
|
||||
"LINK_TEXT": "Learn more about Captain Credits",
|
||||
"NOT_ENABLED": "Captain is not enabled for your account. Please upgrade your plan to access Captain features.",
|
||||
"PROVIDER": {
|
||||
"TITLE": "Provider Configuration",
|
||||
"DESCRIPTION": "Configure the provider connection used by all Copilot and Captain AI requests.",
|
||||
"PROVIDER": "Provider",
|
||||
"MODEL": "Default model",
|
||||
"BASE_URL": "Base URL",
|
||||
"API_KEY": "API key",
|
||||
"API_KEY_PLACEHOLDER": "Enter API key",
|
||||
"API_KEY_CONFIGURED": "Configured: {masked}. Leave blank to keep it unchanged.",
|
||||
"API_KEY_HELP": "The API key is encrypted at rest and is never returned by the API.",
|
||||
"SAVE": "Save provider configuration",
|
||||
"SAVING": "Saving..."
|
||||
},
|
||||
"MODEL_CONFIG": {
|
||||
"TITLE": "Model Configuration",
|
||||
"DESCRIPTION": "Select AI models for different features.",
|
||||
|
||||
@@ -382,15 +382,28 @@
|
||||
},
|
||||
"DOCS": "阅读文档",
|
||||
"SECURITY": "安全",
|
||||
"CAPTAIN_AI": "Captain",
|
||||
"CAPTAIN_AI": "Copilot 配置",
|
||||
"CONVERSATION_WORKFLOW": "会话工作流"
|
||||
},
|
||||
"CAPTAIN_SETTINGS": {
|
||||
"TITLE": "Captain Settings",
|
||||
"DESCRIPTION": "Configure your AI models and features for Captain. Captain follows a credit based billing, you will be charged credits for every action Captain takes based on the model selected.",
|
||||
"LOADING": "Loading Captain configuration...",
|
||||
"LINK_TEXT": "Learn more about Captain Credits",
|
||||
"NOT_ENABLED": "Captain is not enabled for your account. Please upgrade your plan to access Captain features.",
|
||||
"TITLE": "Copilot 配置",
|
||||
"DESCRIPTION": "配置 Copilot 使用的 AI 服务商和模型。保存后立即生效,无需重启服务。",
|
||||
"LOADING": "正在加载 Copilot 配置...",
|
||||
"LINK_TEXT": "了解 Copilot",
|
||||
"NOT_ENABLED": "Copilot 配置暂不可用。",
|
||||
"PROVIDER": {
|
||||
"TITLE": "服务商配置",
|
||||
"DESCRIPTION": "配置所有 Copilot 与 Captain AI 请求使用的模型服务。",
|
||||
"PROVIDER": "服务商",
|
||||
"MODEL": "默认模型",
|
||||
"BASE_URL": "接口地址",
|
||||
"API_KEY": "API Key",
|
||||
"API_KEY_PLACEHOLDER": "请输入 API Key",
|
||||
"API_KEY_CONFIGURED": "已配置:{masked}。留空表示保持不变。",
|
||||
"API_KEY_HELP": "API Key 加密保存,接口不会返回明文。",
|
||||
"SAVE": "保存服务商配置",
|
||||
"SAVING": "保存中..."
|
||||
},
|
||||
"MODEL_CONFIG": {
|
||||
"TITLE": "Model Configuration",
|
||||
"DESCRIPTION": "Select AI models for different features.",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useCaptain } from 'dashboard/composables/useCaptain';
|
||||
import { useCaptainConfigStore } from 'dashboard/store/captain/preferences';
|
||||
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
@@ -11,13 +10,12 @@ import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SectionLayout from '../account/components/SectionLayout.vue';
|
||||
import ModelSelector from './components/ModelSelector.vue';
|
||||
import FeatureToggle from './components/FeatureToggle.vue';
|
||||
import CaptainPaywall from 'next/captain/pageComponents/Paywall.vue';
|
||||
import ProviderConfiguration from './components/ProviderConfiguration.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { captainEnabled } = useCaptain();
|
||||
|
||||
const captainConfigStore = useCaptainConfigStore();
|
||||
const { uiFlags } = storeToRefs(captainConfigStore);
|
||||
const { uiFlags, providerConfig } = storeToRefs(captainConfigStore);
|
||||
const isSavingProvider = ref(false);
|
||||
|
||||
const isLoading = computed(() => uiFlags.value.isFetching);
|
||||
|
||||
@@ -75,6 +73,18 @@ async function handleModelChange({ feature, model }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleProviderSave(config) {
|
||||
isSavingProvider.value = true;
|
||||
try {
|
||||
await captainConfigStore.updatePreferences({ provider_config: config });
|
||||
useAlert(t('CAPTAIN_SETTINGS.API.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(error.response?.data?.error || t('CAPTAIN_SETTINGS.API.ERROR'));
|
||||
} finally {
|
||||
isSavingProvider.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
captainConfigStore.fetch();
|
||||
});
|
||||
@@ -90,13 +100,22 @@ onMounted(() => {
|
||||
<BaseSettingsHeader
|
||||
:title="t('CAPTAIN_SETTINGS.TITLE')"
|
||||
:description="t('CAPTAIN_SETTINGS.DESCRIPTION')"
|
||||
:link-text="t('CAPTAIN_SETTINGS.LINK_TEXT')"
|
||||
icon-name="captain"
|
||||
feature-name="captain_billing"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="captainEnabled" class="flex flex-col gap-1">
|
||||
<div class="flex flex-col gap-1">
|
||||
<SectionLayout
|
||||
:title="t('CAPTAIN_SETTINGS.PROVIDER.TITLE')"
|
||||
:description="t('CAPTAIN_SETTINGS.PROVIDER.DESCRIPTION')"
|
||||
>
|
||||
<ProviderConfiguration
|
||||
:config="providerConfig"
|
||||
:is-saving="isSavingProvider"
|
||||
@save="handleProviderSave"
|
||||
/>
|
||||
</SectionLayout>
|
||||
|
||||
<!-- Model Configuration Section -->
|
||||
<SectionLayout
|
||||
:title="t('CAPTAIN_SETTINGS.MODEL_CONFIG.TITLE')"
|
||||
@@ -133,9 +152,6 @@ onMounted(() => {
|
||||
</div>
|
||||
</SectionLayout>
|
||||
</div>
|
||||
<div v-else>
|
||||
<CaptainPaywall />
|
||||
</div>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
import Index from './Index.vue';
|
||||
|
||||
@@ -10,7 +8,6 @@ export default {
|
||||
path: frontendURL('accounts/:accountId/settings/captain'),
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
props: {
|
||||
@@ -25,11 +22,6 @@ export default {
|
||||
component: Index,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<script setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['save']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const form = reactive({
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4o-mini',
|
||||
apiKey: '',
|
||||
});
|
||||
|
||||
const providerDefaults = {
|
||||
openai: 'https://api.openai.com/v1',
|
||||
anthropic: 'https://api.anthropic.com',
|
||||
openai_compatible: '',
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.config,
|
||||
config => {
|
||||
form.provider = config?.provider || 'openai';
|
||||
form.baseUrl =
|
||||
config?.base_url || providerDefaults[form.provider] || '';
|
||||
form.model = config?.model || 'gpt-4o-mini';
|
||||
form.apiKey = '';
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const handleProviderChange = () => {
|
||||
form.baseUrl = providerDefaults[form.provider] || '';
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
emit('save', {
|
||||
provider: form.provider,
|
||||
base_url: form.baseUrl,
|
||||
model: form.model,
|
||||
api_key: form.apiKey,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-4 p-4 rounded-xl border border-n-weak bg-n-solid-1">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="grid gap-1.5 text-sm text-n-slate-12">
|
||||
<span class="font-medium">{{
|
||||
t('CAPTAIN_SETTINGS.PROVIDER.PROVIDER')
|
||||
}}</span>
|
||||
<select
|
||||
v-model="form.provider"
|
||||
class="w-full px-3 py-2 border rounded-lg border-n-weak bg-n-solid-2"
|
||||
@change="handleProviderChange"
|
||||
>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openai_compatible">OpenAI Compatible</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="grid gap-1.5 text-sm text-n-slate-12">
|
||||
<span class="font-medium">{{
|
||||
t('CAPTAIN_SETTINGS.PROVIDER.MODEL')
|
||||
}}</span>
|
||||
<input
|
||||
v-model.trim="form.model"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border rounded-lg border-n-weak bg-n-solid-2"
|
||||
placeholder="gpt-4o-mini"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="grid gap-1.5 text-sm text-n-slate-12">
|
||||
<span class="font-medium">{{
|
||||
t('CAPTAIN_SETTINGS.PROVIDER.BASE_URL')
|
||||
}}</span>
|
||||
<input
|
||||
v-model.trim="form.baseUrl"
|
||||
type="url"
|
||||
class="w-full px-3 py-2 border rounded-lg border-n-weak bg-n-solid-2"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="grid gap-1.5 text-sm text-n-slate-12">
|
||||
<span class="font-medium">{{
|
||||
t('CAPTAIN_SETTINGS.PROVIDER.API_KEY')
|
||||
}}</span>
|
||||
<input
|
||||
v-model="form.apiKey"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="w-full px-3 py-2 border rounded-lg border-n-weak bg-n-solid-2"
|
||||
:placeholder="
|
||||
config.api_key_configured
|
||||
? t('CAPTAIN_SETTINGS.PROVIDER.API_KEY_CONFIGURED', {
|
||||
masked: config.api_key_masked,
|
||||
})
|
||||
: t('CAPTAIN_SETTINGS.PROVIDER.API_KEY_PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<span class="text-xs text-n-slate-10">
|
||||
{{ t('CAPTAIN_SETTINGS.PROVIDER.API_KEY_HELP') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-white rounded-lg bg-n-brand hover:bg-n-brand/90 disabled:opacity-60"
|
||||
:disabled="
|
||||
isSaving || !form.provider || !form.baseUrl || !form.model
|
||||
"
|
||||
@click="save"
|
||||
>
|
||||
{{
|
||||
isSaving
|
||||
? t('CAPTAIN_SETTINGS.PROVIDER.SAVING')
|
||||
: t('CAPTAIN_SETTINGS.PROVIDER.SAVE')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,6 +6,7 @@ export const useCaptainConfigStore = defineStore('captainConfig', {
|
||||
providers: {},
|
||||
models: {},
|
||||
features: {},
|
||||
providerConfig: {},
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
},
|
||||
@@ -54,6 +55,7 @@ export const useCaptainConfigStore = defineStore('captainConfig', {
|
||||
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 {
|
||||
@@ -66,6 +68,7 @@ export const useCaptainConfigStore = defineStore('captainConfig', {
|
||||
this.providers = response.data.providers || {};
|
||||
this.models = response.data.models || {};
|
||||
this.features = response.data.features || {};
|
||||
this.providerConfig = response.data.provider_config || {};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user