fix(stage-ui): inherit global settings for default card (#2332)
This commit is contained in:
@@ -10,7 +10,6 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from '@proj-airi/stage-ui/stores/modules/default'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
@@ -57,8 +56,7 @@ async function removeAuthenticationProviderConfiguration() {
|
||||
if (!syncedPinia.isLeader())
|
||||
return
|
||||
|
||||
if (await unconfigureAuthenticationProviders())
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
await cardStore.configureForAuthentication(false)
|
||||
}
|
||||
|
||||
function registerAuthenticatedSetup() {
|
||||
@@ -66,8 +64,7 @@ function registerAuthenticatedSetup() {
|
||||
if (!syncedPinia.isLeader())
|
||||
return
|
||||
|
||||
if (await configureAsDefaultsIfEmpty())
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
await cardStore.configureForAuthentication(true)
|
||||
await onboardingStore.closeAfterAuthentication()
|
||||
})
|
||||
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
||||
|
||||
@@ -19,7 +19,6 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from '@proj-airi/stage-ui/stores/modules/default'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
@@ -142,8 +141,7 @@ function createFullStageRuntime() {
|
||||
if (!syncedPinia.isLeader())
|
||||
return
|
||||
|
||||
if (await unconfigureAuthenticationProviders())
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
await cardStore.configureForAuthentication(false)
|
||||
}
|
||||
|
||||
function registerAuthenticatedSetup() {
|
||||
@@ -151,8 +149,7 @@ function createFullStageRuntime() {
|
||||
if (!syncedPinia.isLeader())
|
||||
return
|
||||
|
||||
if (await configureAsDefaultsIfEmpty())
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
await cardStore.configureForAuthentication(true)
|
||||
await onboardingStore.closeAfterAuthentication()
|
||||
})
|
||||
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/conte
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from '@proj-airi/stage-ui/stores/modules/default'
|
||||
import { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
@@ -62,8 +61,7 @@ async function removeAuthenticationProviderConfiguration() {
|
||||
if (!syncedPinia.isLeader())
|
||||
return
|
||||
|
||||
if (await unconfigureAuthenticationProviders())
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
await cardStore.configureForAuthentication(false)
|
||||
}
|
||||
|
||||
function registerAuthenticatedSetup() {
|
||||
@@ -71,8 +69,7 @@ function registerAuthenticatedSetup() {
|
||||
if (!syncedPinia.isLeader())
|
||||
return
|
||||
|
||||
if (await configureAsDefaultsIfEmpty())
|
||||
await cardStore.persistActiveCardModuleSelections()
|
||||
await cardStore.configureForAuthentication(true)
|
||||
await onboardingStore.closeAfterAuthentication()
|
||||
})
|
||||
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
||||
|
||||
@@ -414,10 +414,10 @@ pages:
|
||||
systemprompt: You will receive messages, answer to them like a real human.
|
||||
posthistoryinstructions: Remember to imitate an human.
|
||||
modules_info: >-
|
||||
Configure modules for this character card. If not set, default module
|
||||
configuration will be used.
|
||||
Configure modules for this character card. Empty fields inherit global settings.
|
||||
use_default: Use default
|
||||
use_default_not_configured: Use default (not configured)
|
||||
inherit_global_settings: Inherit global settings
|
||||
fields_info:
|
||||
subtitle: >-
|
||||
You can put here some details about the character you are creating,
|
||||
@@ -441,6 +441,7 @@ pages:
|
||||
consciousness_model: Select the consciousness model (AI model) for this character.
|
||||
speech_model: Select the speech synthesis model and voice for this character.
|
||||
errors:
|
||||
model_required: Select a model for each explicit provider. Models cannot be inherited from a different provider.
|
||||
name: Name should be valid or non-empty.
|
||||
version: 'Error: Invalid version number !'
|
||||
description: 'Error: You must provide a description for this card.'
|
||||
|
||||
@@ -399,9 +399,10 @@ pages:
|
||||
systemprompt: 你将收到消息,请像真实人类一样回复。
|
||||
posthistoryinstructions: 记得模仿人类的行为。
|
||||
modules_info: >-
|
||||
为此角色卡配置模块。如未设置,将使用默认模块配置。
|
||||
配置此角色卡的模块。空字段将继承全局设置。
|
||||
use_default: 使用默认
|
||||
use_default_not_configured: 使用默认(未配置)
|
||||
inherit_global_settings: 继承全局设置
|
||||
fields_info:
|
||||
subtitle: >-
|
||||
您可以在这里填写有关您正在创建的角色的一些详细信息,解释他的背景和情境,以及应该如何回应您的互动。
|
||||
@@ -421,6 +422,7 @@ pages:
|
||||
consciousness_model: 为这个角色选择 AI 模型。
|
||||
speech_model: 为此角色指定语音合成模型与音色。
|
||||
errors:
|
||||
model_required: 请为指定的提供商选择模型,不能继承其他提供商的模型。
|
||||
name: 错误:你必须提供一个有效的名称!
|
||||
version: '错误:版本号无效!'
|
||||
description: '错误:你必须为此卡片提供描述。'
|
||||
|
||||
+157
-107
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { Card } from '@proj-airi/ccc'
|
||||
import type { AiriExtension } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { isCustomProvidersDisabled } from '@proj-airi/stage-shared'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { DEFAULT_ARTISTRY_WIDGET_INSTRUCTION } from '@proj-airi/stage-ui/constants/prompts/artistry-instruction'
|
||||
import { applyAiriCardEditorModules, safeParseAiriCardDraft } from '@proj-airi/stage-ui/services/airi-card-editor'
|
||||
import { applyAiriCardEditorModules, getAiriCardEditorModuleSettings, safeParseAiriCardDraft } from '@proj-airi/stage-ui/services/airi-card-editor'
|
||||
import { resolveModuleSelection } from '@proj-airi/stage-ui/services/airi-card-modules'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
import { useProviderStore } from '@proj-airi/stage-ui/stores/providers/provider'
|
||||
import { useSettingsStageModel } from '@proj-airi/stage-ui/stores/settings/stage-model'
|
||||
import { Button, FieldInput, FieldValues } from '@proj-airi/ui'
|
||||
import { ComboboxSelect } from '@proj-airi/ui/components/form'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@@ -65,15 +65,11 @@ const visionStore = useVisionStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const providersStore = useProviderStore()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const stageModelStore = useSettingsStageModel()
|
||||
const artistryStore = useArtistryStore()
|
||||
|
||||
const { activeProvider: consciousnessProvider, activeModel: defaultConsciousnessModel } = storeToRefs(consciousnessStore)
|
||||
const { activeProvider: visionProvider, activeModel: defaultVisionModel } = storeToRefs(visionStore)
|
||||
const { activeSpeechProvider: speechProvider, activeSpeechModel: defaultSpeechModel, activeSpeechVoiceId: defaultSpeechVoiceId } = storeToRefs(speechStore)
|
||||
const consciousnessProvider = computed(() => cardStore.moduleDefaults?.consciousness.provider ?? '')
|
||||
const visionProvider = computed(() => cardStore.moduleDefaults?.vision.provider ?? '')
|
||||
const speechProvider = computed(() => cardStore.moduleDefaults?.speech.provider ?? '')
|
||||
const { displayModels } = storeToRefs(displayModelsStore)
|
||||
const { stageModelSelected: defaultDisplayModelId } = storeToRefs(stageModelStore)
|
||||
const { activeProvider: defaultArtistryProvider } = storeToRefs(artistryStore)
|
||||
|
||||
// Determine if we're in edit mode
|
||||
const isEditMode = computed(() => !!props.cardId)
|
||||
@@ -89,8 +85,34 @@ const selectedSpeechModel = ref<string>('')
|
||||
const selectedSpeechVoiceId = ref<string>('')
|
||||
const selectedDisplayModelId = ref<string>('')
|
||||
|
||||
// NOTICE:
|
||||
// The editor needs a non-empty option value for inherited settings.
|
||||
// Reka ComboboxItem rejects an empty-string item value.
|
||||
// Source/context: packages/ui/src/components/form/combobox/combobox.vue.
|
||||
// Removal condition: delete this mapping when Reka accepts empty item values.
|
||||
const inheritGlobalSettingOptionValue = '__airi-inherit-global-setting__'
|
||||
|
||||
function createInheritableSelection(selection: Ref<string>) {
|
||||
return computed({
|
||||
get: () => selection.value || inheritGlobalSettingOptionValue,
|
||||
set: (value: string) => {
|
||||
selection.value = value === inheritGlobalSettingOptionValue ? '' : value
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const consciousnessProviderSelection = createInheritableSelection(selectedConsciousnessProvider)
|
||||
const consciousnessModelSelection = createInheritableSelection(selectedConsciousnessModel)
|
||||
const visionProviderSelection = createInheritableSelection(selectedVisionProvider)
|
||||
const visionModelSelection = createInheritableSelection(selectedVisionModel)
|
||||
const speechProviderSelection = createInheritableSelection(selectedSpeechProvider)
|
||||
const speechModelSelection = createInheritableSelection(selectedSpeechModel)
|
||||
const speechVoiceSelection = createInheritableSelection(selectedSpeechVoiceId)
|
||||
const displayModelSelection = createInheritableSelection(selectedDisplayModelId)
|
||||
|
||||
// Artistry configuration
|
||||
const selectedArtistryProvider = ref<string>('')
|
||||
const artistryProviderSelection = createInheritableSelection(selectedArtistryProvider)
|
||||
const selectedArtistryModel = ref<string>('')
|
||||
const selectedArtistryPromptPrefix = ref<string>('')
|
||||
const selectedArtistryWidgetInstruction = ref<string>('')
|
||||
@@ -101,89 +123,106 @@ const selectedArtistryConfigStr = ref<string>('{\n \n}')
|
||||
let isInitializingModuleSelections = false
|
||||
let hasLoadedModuleOptions = false
|
||||
|
||||
interface ModuleSelectOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
function withInheritGlobalSetting(options: ModuleSelectOption[], selected = ''): ModuleSelectOption[] {
|
||||
// Imported ids remain visible even when their provider is not configured here.
|
||||
const missingSelection = selected && !options.some(option => option.value === selected)
|
||||
? [{ value: selected, label: selected }]
|
||||
: []
|
||||
return [
|
||||
{ value: inheritGlobalSettingOptionValue, label: t('settings.pages.card.creation.inherit_global_settings') },
|
||||
...options,
|
||||
...missingSelection,
|
||||
]
|
||||
}
|
||||
|
||||
// Computed: available display model options
|
||||
const displayModelOptions = computed(() =>
|
||||
displayModels.value.map(model => ({
|
||||
withInheritGlobalSetting(displayModels.value.map(model => ({
|
||||
value: model.id,
|
||||
label: model.name,
|
||||
})),
|
||||
})), selectedDisplayModelId.value),
|
||||
)
|
||||
|
||||
// Computed: available consciousness provider options
|
||||
const consciousnessProviderOptions = computed(() => {
|
||||
return providersStore.configuredChatProvidersMetadata.map(provider => ({
|
||||
return withInheritGlobalSetting(providersStore.configuredChatProvidersMetadata.map(provider => ({
|
||||
value: provider.id,
|
||||
label: provider.localizedName || provider.name,
|
||||
}))
|
||||
})), selectedConsciousnessProvider.value)
|
||||
})
|
||||
|
||||
// Computed: available consciousness models options
|
||||
const consciousnessModelOptions = computed(() => {
|
||||
const provider = selectedConsciousnessProvider.value || consciousnessProvider.value
|
||||
if (!provider)
|
||||
return []
|
||||
return withInheritGlobalSetting([], selectedConsciousnessModel.value)
|
||||
const models = providersStore.getModelsForProvider(provider)
|
||||
return models.map(model => ({
|
||||
return withInheritGlobalSetting(models.map(model => ({
|
||||
value: model.id,
|
||||
label: model.name || model.id,
|
||||
}))
|
||||
})), selectedConsciousnessModel.value)
|
||||
})
|
||||
|
||||
// Computed: available vision provider options
|
||||
const visionProviderOptions = computed(() => {
|
||||
return providersStore.configuredVisionProvidersMetadata.map(provider => ({
|
||||
return withInheritGlobalSetting(providersStore.configuredVisionProvidersMetadata.map(provider => ({
|
||||
value: provider.id,
|
||||
label: provider.localizedName || provider.name,
|
||||
}))
|
||||
})), selectedVisionProvider.value)
|
||||
})
|
||||
|
||||
// Computed: available vision models options
|
||||
const visionModelOptions = computed(() => {
|
||||
const provider = selectedVisionProvider.value || visionProvider.value
|
||||
if (!provider)
|
||||
return []
|
||||
return withInheritGlobalSetting([], selectedVisionModel.value)
|
||||
const models = providersStore.getModelsForProvider(provider)
|
||||
return models.map(model => ({
|
||||
return withInheritGlobalSetting(models.map(model => ({
|
||||
value: model.id,
|
||||
label: model.name || model.id,
|
||||
}))
|
||||
})), selectedVisionModel.value)
|
||||
})
|
||||
|
||||
// Computed: available speech provider options
|
||||
const speechProviderOptions = computed(() => {
|
||||
return providersStore.configuredSpeechProvidersMetadata.map(provider => ({
|
||||
return withInheritGlobalSetting(providersStore.configuredSpeechProvidersMetadata.map(provider => ({
|
||||
value: provider.id,
|
||||
label: provider.localizedName || provider.name,
|
||||
}))
|
||||
})), selectedSpeechProvider.value)
|
||||
})
|
||||
|
||||
// Computed: available speech models options
|
||||
const speechModelOptions = computed(() => {
|
||||
const provider = selectedSpeechProvider.value || speechProvider.value
|
||||
if (!provider)
|
||||
return []
|
||||
return withInheritGlobalSetting([], selectedSpeechModel.value)
|
||||
const models = providersStore.getModelsForProvider(provider)
|
||||
return models.map(model => ({
|
||||
return withInheritGlobalSetting(models.map(model => ({
|
||||
value: model.id,
|
||||
label: model.name || model.id,
|
||||
}))
|
||||
})), selectedSpeechModel.value)
|
||||
})
|
||||
|
||||
// Computed: available speech voices options
|
||||
const speechVoiceOptions = computed(() => {
|
||||
const provider = selectedSpeechProvider.value || speechProvider.value
|
||||
if (!provider)
|
||||
return []
|
||||
return withInheritGlobalSetting([], selectedSpeechVoiceId.value)
|
||||
const voices = speechStore.getVoicesForProvider(provider)
|
||||
return voices.map(voice => ({
|
||||
return withInheritGlobalSetting(voices.map(voice => ({
|
||||
value: voice.id,
|
||||
label: voice.name || voice.id,
|
||||
}))
|
||||
})), selectedSpeechVoiceId.value)
|
||||
})
|
||||
|
||||
// Computed: available artistry provider options
|
||||
const artistryProviderOptions = computed(() => {
|
||||
return [
|
||||
return withInheritGlobalSetting([
|
||||
{ value: 'none', label: 'None (Disabled)' },
|
||||
{ value: 'comfyui', label: 'ComfyUI' },
|
||||
...(isCustomProvidersDisabled()
|
||||
@@ -192,7 +231,7 @@ const artistryProviderOptions = computed(() => {
|
||||
{ value: 'replicate', label: 'Replicate' },
|
||||
{ value: 'nanobanana', label: 'Nano Banana' },
|
||||
]),
|
||||
]
|
||||
], selectedArtistryProvider.value)
|
||||
})
|
||||
|
||||
async function loadSelectedModuleOptions() {
|
||||
@@ -201,16 +240,19 @@ async function loadSelectedModuleOptions() {
|
||||
|
||||
hasLoadedModuleOptions = true
|
||||
const loads: Promise<unknown>[] = []
|
||||
if (selectedConsciousnessProvider.value)
|
||||
loads.push(consciousnessStore.loadModelsForProvider(selectedConsciousnessProvider.value))
|
||||
const consciousnessProviderId = selectedConsciousnessProvider.value || consciousnessProvider.value
|
||||
if (consciousnessProviderId)
|
||||
loads.push(consciousnessStore.loadModelsForProvider(consciousnessProviderId))
|
||||
|
||||
if (selectedVisionProvider.value)
|
||||
loads.push(visionStore.loadModelsForProvider(selectedVisionProvider.value))
|
||||
const visionProviderId = selectedVisionProvider.value || visionProvider.value
|
||||
if (visionProviderId)
|
||||
loads.push(visionStore.loadModelsForProvider(visionProviderId))
|
||||
|
||||
if (selectedSpeechProvider.value) {
|
||||
loads.push(speechStore.loadVoicesForProvider(selectedSpeechProvider.value, selectedSpeechModel.value || undefined))
|
||||
if (providersStore.supportsModelListing(selectedSpeechProvider.value))
|
||||
loads.push(providersStore.fetchModelsForProvider(selectedSpeechProvider.value))
|
||||
const speechProviderId = selectedSpeechProvider.value || speechProvider.value
|
||||
if (speechProviderId) {
|
||||
loads.push(speechStore.loadVoicesForProvider(speechProviderId, selectedSpeechModel.value || undefined))
|
||||
if (providersStore.supportsModelListing(speechProviderId))
|
||||
loads.push(providersStore.fetchModelsForProvider(speechProviderId))
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -222,48 +264,48 @@ async function loadSelectedModuleOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedArtistryProvider, (provider, previous) => {
|
||||
if (props.modelValue && !isInitializingModuleSelections && provider !== previous)
|
||||
selectedArtistryModel.value = ''
|
||||
}, { flush: 'sync' })
|
||||
|
||||
// Watch consciousness provider changes and reload models
|
||||
watch(selectedConsciousnessProvider, async (newProvider, oldProvider) => {
|
||||
if (props.modelValue && !isInitializingModuleSelections && oldProvider !== undefined && newProvider !== oldProvider && newProvider) {
|
||||
await consciousnessStore.loadModelsForProvider(newProvider)
|
||||
// Reset model selection to default or empty
|
||||
if (props.modelValue && !isInitializingModuleSelections && newProvider !== oldProvider) {
|
||||
selectedConsciousnessModel.value = ''
|
||||
await consciousnessStore.loadModelsForProvider(newProvider || consciousnessProvider.value)
|
||||
}
|
||||
})
|
||||
}, { flush: 'sync' })
|
||||
|
||||
// Watch vision provider changes and reload models
|
||||
watch(selectedVisionProvider, async (newProvider, oldProvider) => {
|
||||
if (props.modelValue && !isInitializingModuleSelections && oldProvider !== undefined && newProvider !== oldProvider && newProvider) {
|
||||
await visionStore.loadModelsForProvider(newProvider)
|
||||
if (props.modelValue && !isInitializingModuleSelections && newProvider !== oldProvider) {
|
||||
selectedVisionModel.value = ''
|
||||
await visionStore.loadModelsForProvider(newProvider || visionProvider.value)
|
||||
}
|
||||
})
|
||||
}, { flush: 'sync' })
|
||||
|
||||
// Watch speech provider changes and reload models/voices
|
||||
watch(selectedSpeechProvider, async (newProvider, oldProvider) => {
|
||||
if (props.modelValue && !isInitializingModuleSelections && oldProvider !== undefined && newProvider !== oldProvider && newProvider) {
|
||||
await speechStore.loadVoicesForProvider(newProvider)
|
||||
if (providersStore.supportsModelListing(newProvider)) {
|
||||
await providersStore.fetchModelsForProvider(newProvider)
|
||||
}
|
||||
// Reset model and voice selection
|
||||
if (props.modelValue && !isInitializingModuleSelections && newProvider !== oldProvider) {
|
||||
selectedSpeechModel.value = ''
|
||||
selectedSpeechVoiceId.value = ''
|
||||
const provider = newProvider || speechProvider.value
|
||||
await speechStore.loadVoicesForProvider(provider)
|
||||
if (provider && providersStore.supportsModelListing(provider))
|
||||
await providersStore.fetchModelsForProvider(provider)
|
||||
}
|
||||
})
|
||||
}, { flush: 'sync' })
|
||||
|
||||
// Reset voice when speech model changes (different models may have different voices)
|
||||
watch(selectedSpeechModel, async (newModel, oldModel) => {
|
||||
// Only reset if model actually changed and we're not initializing
|
||||
const provider = selectedSpeechProvider.value || speechProvider.value
|
||||
if (props.modelValue && !isInitializingModuleSelections && oldModel !== undefined && newModel !== oldModel && provider) {
|
||||
// Reload voices for the current provider
|
||||
await speechStore.loadVoicesForProvider(provider)
|
||||
|
||||
// Reset voice selection to default
|
||||
selectedSpeechVoiceId.value = defaultSpeechVoiceId.value || ''
|
||||
selectedSpeechVoiceId.value = ''
|
||||
await speechStore.loadVoicesForProvider(provider, newModel || undefined)
|
||||
}
|
||||
})
|
||||
}, { flush: 'sync' })
|
||||
|
||||
// Tab type definition
|
||||
interface Tab {
|
||||
@@ -322,6 +364,20 @@ const showError = ref<boolean>(false)
|
||||
const errorMessage = ref<string>('')
|
||||
|
||||
async function saveCard(card: Card, activate: boolean): Promise<boolean> {
|
||||
const defaults = cardStore.moduleDefaults
|
||||
if (defaults) {
|
||||
// A card may inherit an unconfigured global module. A different explicit
|
||||
// provider must have its own model; global model ids are not portable.
|
||||
const missingModel = [
|
||||
{ selection: { provider: selectedConsciousnessProvider.value, model: selectedConsciousnessModel.value }, defaults: defaults.consciousness },
|
||||
{ selection: { provider: selectedVisionProvider.value, model: selectedVisionModel.value }, defaults: defaults.vision },
|
||||
].some(({ selection, defaults }) => selection.provider && !resolveModuleSelection(selection, defaults).model)
|
||||
if (missingModel) {
|
||||
showError.value = true
|
||||
errorMessage.value = t('settings.pages.card.creation.errors.model_required')
|
||||
return false
|
||||
}
|
||||
}
|
||||
const draftResult = safeParseAiriCardDraft(toRaw(card), selectedArtistryConfigStr.value)
|
||||
if (!draftResult.success) {
|
||||
showError.value = true
|
||||
@@ -334,21 +390,21 @@ async function saveCard(card: Card, activate: boolean): Promise<boolean> {
|
||||
|
||||
const cardWithModules = applyAiriCardEditorModules(rawCard, {
|
||||
consciousness: {
|
||||
provider: selectedConsciousnessProvider.value || consciousnessProvider.value,
|
||||
model: selectedConsciousnessModel.value || defaultConsciousnessModel.value,
|
||||
provider: selectedConsciousnessProvider.value,
|
||||
model: selectedConsciousnessModel.value,
|
||||
},
|
||||
vision: {
|
||||
provider: selectedVisionProvider.value || visionProvider.value,
|
||||
model: selectedVisionModel.value || defaultVisionModel.value,
|
||||
provider: selectedVisionProvider.value,
|
||||
model: selectedVisionModel.value,
|
||||
},
|
||||
speech: {
|
||||
provider: selectedSpeechProvider.value || speechProvider.value,
|
||||
model: selectedSpeechModel.value || defaultSpeechModel.value,
|
||||
voice_id: selectedSpeechVoiceId.value || defaultSpeechVoiceId.value,
|
||||
provider: selectedSpeechProvider.value,
|
||||
model: selectedSpeechModel.value,
|
||||
voice_id: selectedSpeechVoiceId.value,
|
||||
},
|
||||
displayModelId: selectedDisplayModelId.value || defaultDisplayModelId.value,
|
||||
displayModelId: selectedDisplayModelId.value,
|
||||
artistry: {
|
||||
provider: selectedArtistryProvider.value || defaultArtistryProvider.value,
|
||||
provider: selectedArtistryProvider.value,
|
||||
model: selectedArtistryModel.value,
|
||||
promptPrefix: selectedArtistryPromptPrefix.value,
|
||||
widgetInstruction: selectedArtistryWidgetInstruction.value,
|
||||
@@ -404,19 +460,19 @@ function initializeCard(): Card {
|
||||
const existingCard = (isEditMode.value && props.cardId) ? cardStore.getCard(props.cardId) : undefined
|
||||
const airiExt = existingCard?.extensions?.airi as AiriExtensionWithLegacyArtistry | undefined
|
||||
|
||||
// Initialize module selections with fallback logic (handles all cases: create, edit with/without extension)
|
||||
selectedConsciousnessProvider.value = airiExt?.modules?.consciousness?.provider || consciousnessProvider.value
|
||||
selectedConsciousnessModel.value = airiExt?.modules?.consciousness?.model || defaultConsciousnessModel.value
|
||||
selectedVisionProvider.value = airiExt?.modules?.vision?.provider || visionProvider.value
|
||||
selectedVisionModel.value = airiExt?.modules?.vision?.model || defaultVisionModel.value
|
||||
selectedSpeechProvider.value = airiExt?.modules?.speech?.provider || speechProvider.value
|
||||
selectedSpeechModel.value = airiExt?.modules?.speech?.model || defaultSpeechModel.value
|
||||
selectedSpeechVoiceId.value = airiExt?.modules?.speech?.voice_id || defaultSpeechVoiceId.value
|
||||
selectedDisplayModelId.value = airiExt?.modules?.displayModelId || defaultDisplayModelId.value
|
||||
const moduleSettings = getAiriCardEditorModuleSettings(existingCard)
|
||||
selectedConsciousnessProvider.value = moduleSettings.consciousness.provider
|
||||
selectedConsciousnessModel.value = moduleSettings.consciousness.model
|
||||
selectedVisionProvider.value = moduleSettings.vision.provider
|
||||
selectedVisionModel.value = moduleSettings.vision.model
|
||||
selectedSpeechProvider.value = moduleSettings.speech.provider
|
||||
selectedSpeechModel.value = moduleSettings.speech.model
|
||||
selectedSpeechVoiceId.value = moduleSettings.speech.voice_id
|
||||
selectedDisplayModelId.value = moduleSettings.displayModelId ?? ''
|
||||
|
||||
// NOTICE: keep legacy `extensions.airi.artistry` fallback so existing cards continue to load.
|
||||
const artistrySettings = airiExt?.modules?.artistry || airiExt?.artistry
|
||||
selectedArtistryProvider.value = artistrySettings?.provider || defaultArtistryProvider.value
|
||||
selectedArtistryProvider.value = artistrySettings?.provider ?? ''
|
||||
selectedArtistryModel.value = artistrySettings?.model || ''
|
||||
selectedArtistryPromptPrefix.value = artistrySettings?.promptPrefix || ''
|
||||
selectedArtistryWidgetInstruction.value = artistrySettings?.widgetInstruction || DEFAULT_ARTISTRY_WIDGET_INSTRUCTION
|
||||
@@ -425,10 +481,10 @@ function initializeCard(): Card {
|
||||
selectedArtistryAutonomousThreshold.value = (artistrySettings as any)?.autonomousThreshold ?? 70
|
||||
|
||||
try {
|
||||
selectedArtistryConfigStr.value = artistrySettings?.options ? JSON.stringify(artistrySettings.options, null, 2) : '{\n \n}'
|
||||
selectedArtistryConfigStr.value = artistrySettings?.options ? JSON.stringify(artistrySettings.options, null, 2) : ''
|
||||
}
|
||||
catch {
|
||||
selectedArtistryConfigStr.value = '{\n \n}'
|
||||
selectedArtistryConfigStr.value = ''
|
||||
}
|
||||
|
||||
// Return existing card data or defaults
|
||||
@@ -490,10 +546,8 @@ const cardSystemPrompt = makeComputed('systemPrompt')
|
||||
const cardPostHistoryInstructions = makeComputed('postHistoryInstructions')
|
||||
|
||||
// Helper function to generate placeholder text for default values
|
||||
function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
return defaultValue
|
||||
? `${t('settings.pages.card.creation.use_default')} (${defaultValue})`
|
||||
: t('settings.pages.card.creation.use_default_not_configured')
|
||||
function getDefaultPlaceholder(): string {
|
||||
return t('settings.pages.card.creation.inherit_global_settings')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -574,9 +628,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
{{ t('settings.pages.card.chat.provider') }}
|
||||
</label>
|
||||
<ComboboxSelect
|
||||
v-model="selectedConsciousnessProvider"
|
||||
v-model="consciousnessProviderSelection"
|
||||
:options="consciousnessProviderOptions"
|
||||
:placeholder="getDefaultPlaceholder(consciousnessProvider)"
|
||||
:placeholder="getDefaultPlaceholder()"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -588,10 +642,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
{{ t('settings.pages.card.consciousness.model') }}
|
||||
</label>
|
||||
<ComboboxSelect
|
||||
v-model="selectedConsciousnessModel"
|
||||
v-model="consciousnessModelSelection"
|
||||
:options="consciousnessModelOptions"
|
||||
:placeholder="getDefaultPlaceholder(defaultConsciousnessModel)"
|
||||
:disabled="!selectedConsciousnessProvider && !consciousnessProvider"
|
||||
:placeholder="getDefaultPlaceholder()"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -603,9 +656,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
{{ t('settings.pages.card.vision.provider') }}
|
||||
</label>
|
||||
<ComboboxSelect
|
||||
v-model="selectedVisionProvider"
|
||||
v-model="visionProviderSelection"
|
||||
:options="visionProviderOptions"
|
||||
:placeholder="getDefaultPlaceholder(visionProvider)"
|
||||
:placeholder="getDefaultPlaceholder()"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -617,10 +670,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
{{ t('settings.pages.card.vision.model') }}
|
||||
</label>
|
||||
<ComboboxSelect
|
||||
v-model="selectedVisionModel"
|
||||
v-model="visionModelSelection"
|
||||
:options="visionModelOptions"
|
||||
:placeholder="getDefaultPlaceholder(defaultVisionModel)"
|
||||
:disabled="!selectedVisionProvider && !visionProvider"
|
||||
:placeholder="getDefaultPlaceholder()"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -632,9 +684,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
{{ t('settings.pages.card.speech.provider') }}
|
||||
</label>
|
||||
<ComboboxSelect
|
||||
v-model="selectedSpeechProvider"
|
||||
v-model="speechProviderSelection"
|
||||
:options="speechProviderOptions"
|
||||
:placeholder="getDefaultPlaceholder(speechProvider)"
|
||||
:placeholder="getDefaultPlaceholder()"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -646,10 +698,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
{{ t('settings.pages.card.speech.model') }}
|
||||
</label>
|
||||
<ComboboxSelect
|
||||
v-model="selectedSpeechModel"
|
||||
v-model="speechModelSelection"
|
||||
:options="speechModelOptions"
|
||||
:placeholder="getDefaultPlaceholder(defaultSpeechModel)"
|
||||
:disabled="!selectedSpeechProvider && !speechProvider"
|
||||
:placeholder="getDefaultPlaceholder()"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -661,10 +712,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
{{ t('settings.pages.card.speech.voice') }}
|
||||
</label>
|
||||
<ComboboxSelect
|
||||
v-model="selectedSpeechVoiceId"
|
||||
v-model="speechVoiceSelection"
|
||||
:options="speechVoiceOptions"
|
||||
:placeholder="getDefaultPlaceholder(defaultSpeechVoiceId)"
|
||||
:disabled="!selectedSpeechProvider && !speechProvider"
|
||||
:placeholder="getDefaultPlaceholder()"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -676,9 +726,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
{{ t('settings.pages.card.body-model') }}
|
||||
</label>
|
||||
<ComboboxSelect
|
||||
v-model="selectedDisplayModelId"
|
||||
v-model="displayModelSelection"
|
||||
:options="displayModelOptions"
|
||||
:placeholder="getDefaultPlaceholder(defaultDisplayModelId)"
|
||||
:placeholder="getDefaultPlaceholder()"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -695,7 +745,7 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
<!-- Artistry -->
|
||||
<CardCreationTabArtistry
|
||||
v-else-if="activeTab === 'artistry'"
|
||||
v-model:selected-artistry-provider="selectedArtistryProvider"
|
||||
v-model:selected-artistry-provider="artistryProviderSelection"
|
||||
v-model:selected-artistry-model="selectedArtistryModel"
|
||||
v-model:selected-artistry-prompt-prefix="selectedArtistryPromptPrefix"
|
||||
v-model:selected-artistry-widget-instruction="selectedArtistryWidgetInstruction"
|
||||
@@ -704,7 +754,7 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
v-model:selected-artistry-spawn-mode="selectedArtistrySpawnMode"
|
||||
v-model:selected-artistry-config-str="selectedArtistryConfigStr"
|
||||
:artistry-provider-options="artistryProviderOptions"
|
||||
:default-artistry-provider-placeholder="getDefaultPlaceholder(defaultArtistryProvider)"
|
||||
:default-artistry-provider-placeholder="getDefaultPlaceholder()"
|
||||
/>
|
||||
|
||||
<div class="ml-auto mr-1 flex flex-row gap-2">
|
||||
|
||||
@@ -9,9 +9,6 @@ import { exportAiriCardPackage } from '@proj-airi/stage-ui/services/airi-card-im
|
||||
import { useBackgroundStore } from '@proj-airi/stage-ui/stores/background'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||
import { Button, Select } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
@@ -41,17 +38,11 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n()
|
||||
const { trackSceneBackgroundSet } = useAnalytics()
|
||||
const cardStore = useAiriCardStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const visionStore = useVisionStore()
|
||||
const backgroundStore = useBackgroundStore()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
|
||||
const { removeCard } = cardStore
|
||||
const { activeCardId } = storeToRefs(cardStore)
|
||||
const { activeProvider: consciousnessProvider, activeModel: defaultConsciousnessModel } = storeToRefs(consciousnessStore)
|
||||
const { activeSpeechProvider: speechProvider, activeSpeechModel: defaultSpeechModel, activeSpeechVoiceId: defaultVoiceId } = storeToRefs(speechStore)
|
||||
const { activeProvider: visionProvider, activeModel: defaultVisionModel } = storeToRefs(visionStore)
|
||||
|
||||
const isRefreshingGallery = ref(false)
|
||||
const isExportingCard = shallowRef(false)
|
||||
@@ -317,15 +308,13 @@ watch(() => props.modelValue, (isOpen) => {
|
||||
})
|
||||
|
||||
// Helper function to generate placeholder text for default values
|
||||
function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
return defaultValue
|
||||
? `${t('settings.pages.card.creation.use_default')} (${defaultValue})`
|
||||
: t('settings.pages.card.creation.use_default_not_configured')
|
||||
function getDefaultPlaceholder(): string {
|
||||
return t('settings.pages.card.creation.inherit_global_settings')
|
||||
}
|
||||
|
||||
// Helper function to get display value for module settings
|
||||
function getModuleDisplayValue(value: string | undefined, defaultValue: string | undefined): string {
|
||||
return value || getDefaultPlaceholder(defaultValue)
|
||||
function getModuleDisplayValue(value: string | undefined): string {
|
||||
return value || getDefaultPlaceholder()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -466,7 +455,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
{{ t('settings.pages.card.chat.provider') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ getModuleDisplayValue(moduleSettings.consciousnessProvider, consciousnessProvider) }}
|
||||
{{ getModuleDisplayValue(moduleSettings.consciousnessProvider) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -483,7 +472,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
{{ t('settings.pages.card.consciousness.model') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ getModuleDisplayValue(moduleSettings.consciousness, defaultConsciousnessModel) }}
|
||||
{{ getModuleDisplayValue(moduleSettings.consciousness) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -500,7 +489,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
{{ t('settings.pages.card.vision.provider') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ getModuleDisplayValue(moduleSettings.visionProvider, visionProvider) }}
|
||||
{{ getModuleDisplayValue(moduleSettings.visionProvider) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -517,7 +506,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
{{ t('settings.pages.card.vision.model') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ getModuleDisplayValue(moduleSettings.vision, defaultVisionModel) }}
|
||||
{{ getModuleDisplayValue(moduleSettings.vision) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -534,7 +523,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
{{ t('settings.pages.card.speech.provider') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ getModuleDisplayValue(moduleSettings.speechProvider, speechProvider) }}
|
||||
{{ getModuleDisplayValue(moduleSettings.speechProvider) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -551,7 +540,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
{{ t('settings.pages.card.speech.model') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ getModuleDisplayValue(moduleSettings.speech, defaultSpeechModel) }}
|
||||
{{ getModuleDisplayValue(moduleSettings.speech) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -568,7 +557,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
{{ t('settings.pages.card.speech.voice') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ getModuleDisplayValue(moduleSettings.voice, defaultVoiceId) }}
|
||||
{{ getModuleDisplayValue(moduleSettings.voice) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,28 +42,26 @@ watch(activeProvider, async (provider) => {
|
||||
await consciousnessStore.loadModelsForProvider(provider)
|
||||
}, { immediate: true })
|
||||
|
||||
watch([activeProvider, activeModel], ([provider, model]) => {
|
||||
void airiCardStore.updateActiveCardConsciousness({ provider, model })
|
||||
})
|
||||
async function persistSelection() {
|
||||
await airiCardStore.updateActiveCardConsciousness({ provider: activeProvider.value, model: activeModel.value })
|
||||
}
|
||||
|
||||
function updateCustomModelName(value: string) {
|
||||
customModelName.value = value
|
||||
}
|
||||
|
||||
function selectModel(modelId: string) {
|
||||
async function selectModel(modelId: string) {
|
||||
const previousModelId = activeModel.value
|
||||
activeModel.value = modelId
|
||||
await persistSelection()
|
||||
|
||||
if (previousModelId !== modelId)
|
||||
trackModelSwitched(previousModelId || 'none', modelId)
|
||||
}
|
||||
|
||||
function handleDeleteProvider(providerId: string) {
|
||||
if (activeProvider.value === providerId) {
|
||||
activeProvider.value = ''
|
||||
activeModel.value = ''
|
||||
}
|
||||
providersStore.deleteProvider(providerId)
|
||||
async function handleDeleteProvider(providerId: string) {
|
||||
await airiCardStore.clearProviderSelections(providerId)
|
||||
await providersStore.deleteProvider(providerId)
|
||||
}
|
||||
|
||||
async function updateReasoning(value: boolean) {
|
||||
@@ -104,6 +102,7 @@ async function updateReasoning(value: boolean) {
|
||||
:value="metadata.id"
|
||||
:title="metadata.localizedName || 'Unknown'"
|
||||
:description="metadata.localizedDescription"
|
||||
@update:model-value="persistSelection"
|
||||
@click="trackProviderClick(metadata.id, 'consciousness')"
|
||||
>
|
||||
<template v-if="!metadata.id.startsWith('official-provider')" #topRight>
|
||||
@@ -194,6 +193,7 @@ async function updateReasoning(value: boolean) {
|
||||
v-model="activeModel" type="text"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
|
||||
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
||||
@input="persistSelection"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
@@ -285,6 +285,7 @@ async function updateReasoning(value: boolean) {
|
||||
v-model="activeModel" type="text"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
|
||||
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
||||
@input="persistSelection"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -279,6 +279,7 @@ function selectSpeechProvider(providerId: string) {
|
||||
* Tracks explicit voice selection from catalog or custom input controls.
|
||||
*/
|
||||
async function selectSpeechVoice(voiceId: string | undefined) {
|
||||
await persistSelection()
|
||||
if (!voiceId)
|
||||
return
|
||||
|
||||
@@ -290,11 +291,15 @@ async function selectSpeechVoice(voiceId: string | undefined) {
|
||||
})
|
||||
}
|
||||
|
||||
function selectSpeechSource(sourceId: string) {
|
||||
async function selectSpeechSource(sourceId: string) {
|
||||
activeSpeechProvider.value = sourceId
|
||||
activeSpeechModel.value = ''
|
||||
activeSpeechVoiceId.value = ''
|
||||
activeSpeechVoice.value = undefined
|
||||
await persistSelection()
|
||||
}
|
||||
|
||||
function selectSpeechModel(modelOptionId: string) {
|
||||
async function selectSpeechModel(modelOptionId: string) {
|
||||
const streamingModelId = modelIdFromStreamingOptionId(modelOptionId)
|
||||
const nextProvider = streamingModelId == null
|
||||
? activeSpeechProvider.value === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
|
||||
@@ -310,6 +315,9 @@ function selectSpeechModel(modelOptionId: string) {
|
||||
}
|
||||
|
||||
activeSpeechModel.value = nextModel
|
||||
activeSpeechVoiceId.value = ''
|
||||
activeSpeechVoice.value = undefined
|
||||
await persistSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,26 +374,11 @@ onMounted(async () => {
|
||||
trackOfficialTtsExposure()
|
||||
})
|
||||
|
||||
watch(activeSpeechProvider, async (newProvider, oldProvider) => {
|
||||
watch(activeSpeechProvider, async (newProvider) => {
|
||||
await providersStore.loadModelsForConfiguredProviders()
|
||||
if (newProvider !== activeSpeechProvider.value)
|
||||
return
|
||||
|
||||
// Reset model and voice when switching providers (but not on initial load)
|
||||
const isMergedOfficialSwitch = (
|
||||
oldProvider === OFFICIAL_SPEECH_PROVIDER_ID
|
||||
|| oldProvider === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
|
||||
) && (
|
||||
newProvider === OFFICIAL_SPEECH_PROVIDER_ID
|
||||
|| newProvider === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
|
||||
)
|
||||
if (oldProvider !== undefined && oldProvider !== newProvider && !isMergedOfficialSwitch) {
|
||||
activeSpeechModel.value = ''
|
||||
activeSpeechVoiceId.value = ''
|
||||
activeSpeechVoice.value = undefined
|
||||
}
|
||||
|
||||
// Re-seed the streaming default model after the reset above so its voices
|
||||
// load model-scoped (the server only returns recommended voices for an
|
||||
// explicit ?model=). No-op for other providers / when a model is selected.
|
||||
speechStore.ensureActiveSpeechModel()
|
||||
await speechStore.loadVoicesForProvider(newProvider, activeSpeechModel.value || undefined)
|
||||
trackOfficialTtsExposure(newProvider, currentTtsModelId())
|
||||
@@ -397,16 +390,17 @@ watch(activeSpeechModel, async (model) => {
|
||||
if (!activeSpeechProvider.value)
|
||||
return
|
||||
|
||||
activeSpeechVoiceId.value = ''
|
||||
activeSpeechVoice.value = undefined
|
||||
|
||||
await speechStore.loadVoicesForProvider(activeSpeechProvider.value, model || undefined)
|
||||
trackOfficialTtsExposure(activeSpeechProvider.value, currentTtsModelId())
|
||||
})
|
||||
|
||||
watch([activeSpeechProvider, activeSpeechModel, activeSpeechVoiceId], ([provider, model, voiceId]) => {
|
||||
void airiCardStore.updateActiveCardSpeech({ provider, model, voice_id: voiceId })
|
||||
async function persistSelection() {
|
||||
await airiCardStore.updateActiveCardSpeech({
|
||||
provider: activeSpeechProvider.value,
|
||||
model: activeSpeechModel.value,
|
||||
voice_id: activeSpeechVoiceId.value,
|
||||
})
|
||||
}
|
||||
|
||||
// Function to generate speech
|
||||
async function generateTestSpeech() {
|
||||
@@ -589,21 +583,17 @@ function commitCustomVoiceSelection() {
|
||||
|
||||
function updateCustomModelName(value: string | undefined) {
|
||||
activeSpeechModel.value = value || ''
|
||||
activeSpeechVoiceId.value = ''
|
||||
void persistSelection()
|
||||
}
|
||||
|
||||
function handleDeleteProvider(providerId: string) {
|
||||
async function handleDeleteProvider(providerId: string) {
|
||||
if (providerId === 'speech-noop') {
|
||||
return
|
||||
}
|
||||
|
||||
if (activeSpeechProvider.value === providerId) {
|
||||
activeSpeechProvider.value = 'speech-noop'
|
||||
activeSpeechModel.value = ''
|
||||
activeSpeechVoiceId.value = ''
|
||||
activeSpeechVoice.value = undefined
|
||||
}
|
||||
|
||||
providersStore.deleteProvider(providerId)
|
||||
await airiCardStore.clearProviderSelections(providerId)
|
||||
await providersStore.deleteProvider(providerId)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -899,6 +889,7 @@ function handleDeleteProvider(providerId: string) {
|
||||
<select
|
||||
v-model="activeSpeechModel"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
|
||||
@change="selectSpeechModel(activeSpeechModel)"
|
||||
>
|
||||
<option value="eleven_monolingual_v1">
|
||||
Monolingual v1
|
||||
|
||||
@@ -41,31 +41,30 @@ const {
|
||||
const { t } = useI18n()
|
||||
const { trackProviderClick } = useAnalytics()
|
||||
|
||||
watch(activeProvider, async (provider, oldProvider) => {
|
||||
watch(activeProvider, async (provider) => {
|
||||
if (!provider)
|
||||
return
|
||||
|
||||
if (oldProvider !== undefined && oldProvider !== provider) {
|
||||
visionStore.resetModelSelection()
|
||||
}
|
||||
|
||||
await visionStore.loadModelsForProvider(provider)
|
||||
}, { immediate: true })
|
||||
|
||||
watch([activeProvider, activeModel], ([provider, model]) => {
|
||||
void airiCardStore.updateActiveCardVision({ provider, model })
|
||||
})
|
||||
async function selectProvider(provider: string) {
|
||||
activeProvider.value = provider
|
||||
visionStore.resetModelSelection()
|
||||
await persistSelection()
|
||||
}
|
||||
|
||||
async function persistSelection() {
|
||||
await airiCardStore.updateActiveCardVision({ provider: activeProvider.value, model: activeModel.value })
|
||||
}
|
||||
|
||||
function updateCustomModelName(value: string) {
|
||||
customModelName.value = value
|
||||
}
|
||||
|
||||
function handleDeleteProvider(providerId: string) {
|
||||
if (activeProvider.value === providerId) {
|
||||
activeProvider.value = ''
|
||||
activeModel.value = ''
|
||||
}
|
||||
providersStore.deleteProvider(providerId)
|
||||
async function handleDeleteProvider(providerId: string) {
|
||||
await airiCardStore.clearProviderSelections(providerId)
|
||||
await providersStore.deleteProvider(providerId)
|
||||
}
|
||||
|
||||
const formattedLastCapture = computed(() => formatRelativeTime(lastCaptureAt.value))
|
||||
@@ -114,11 +113,12 @@ function formatRelativeTime(timestamp: number | null) {
|
||||
v-for="metadata in moduleVisionProvidersMetadata"
|
||||
:id="metadata.id"
|
||||
:key="metadata.id"
|
||||
v-model="activeProvider"
|
||||
:model-value="activeProvider"
|
||||
name="provider"
|
||||
:value="metadata.id"
|
||||
:title="metadata.localizedName || 'Unknown'"
|
||||
:description="metadata.localizedDescription"
|
||||
@update:model-value="selectProvider"
|
||||
@click="trackProviderClick(metadata.id, 'vision')"
|
||||
>
|
||||
<template v-if="canDeleteProvider(metadata.id)" #topRight>
|
||||
@@ -275,6 +275,7 @@ function formatRelativeTime(timestamp: number | null) {
|
||||
:expand-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.expand')"
|
||||
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
|
||||
expanded-class="mb-12"
|
||||
@update:model-value="persistSelection"
|
||||
@update:custom-value="updateCustomModelName"
|
||||
/>
|
||||
</template>
|
||||
@@ -336,6 +337,7 @@ function formatRelativeTime(timestamp: number | null) {
|
||||
'dark:bg-neutral-900',
|
||||
]"
|
||||
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
||||
@input="persistSelection"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
Shared core for stage
|
||||
|
||||
## Character-card module settings
|
||||
|
||||
The card store owns three distinct states:
|
||||
|
||||
- `moduleDefaults` stores global provider, model, voice, and display selections.
|
||||
- Each card stores explicit overrides. An empty string means inherit.
|
||||
- Module stores expose the resolved runtime selections used by the application.
|
||||
|
||||
Use `configureForAuthentication` for login and logout. It updates global
|
||||
defaults, then reapplies the active card without saving defaults into that card.
|
||||
Use card commands for activation and explicit edits. Settings pages must not
|
||||
save cards from watchers: authentication and remote snapshots also trigger them.
|
||||
The synchronization leader owns these commands; followers receive snapshots.
|
||||
|
||||
Models inherit only within the same provider. Voices also require the same
|
||||
model. A different provider without a model stays unconfigured rather than
|
||||
receiving an unrelated model id. The editor requires a model for an explicit
|
||||
chat or vision provider unless that model can be inherited safely.
|
||||
|
||||
Defaults are seeded once from the current runtime on upgrade. This cannot
|
||||
recover historical global values that an older card already overwrote.
|
||||
Existing `speech-noop` selections are preserved because they may represent
|
||||
intentional silence. Users can explicitly choose **Inherit global settings**
|
||||
in the editor; importing or saving an unrelated card field does not change it.
|
||||
|
||||
## Button analytics
|
||||
|
||||
Register the shared plugin once in each Vue application:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Shared with auth setup without importing provider factories or chat stores.
|
||||
export const OFFICIAL_CHAT_PROVIDER_ID = 'official-provider'
|
||||
export const OFFICIAL_SPEECH_PROVIDER_ID = 'official-provider-speech'
|
||||
export const OFFICIAL_SPEECH_STREAMING_PROVIDER_ID = 'official-provider-speech-streaming'
|
||||
export const OFFICIAL_TRANSCRIPTION_PROVIDER_ID = 'official-provider-transcription'
|
||||
export const OFFICIAL_VISION_PROVIDER_ID = 'vision-official-provider'
|
||||
@@ -8,13 +8,10 @@ import { z } from 'zod'
|
||||
import { getAuthToken } from '../../../../libs/auth'
|
||||
import { SERVER_URL } from '../../../../libs/server'
|
||||
import { defineProvider } from '../registry'
|
||||
import { OFFICIAL_CHAT_PROVIDER_ID, OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID, OFFICIAL_TRANSCRIPTION_PROVIDER_ID } from './constants'
|
||||
import { createOfficialAudioProvider, createOfficialOpenAIProvider, OFFICIAL_ICON, withCredentials } from './shared'
|
||||
|
||||
export const OFFICIAL_CHAT_PROVIDER_ID = 'official-provider'
|
||||
export const OFFICIAL_SPEECH_PROVIDER_ID = 'official-provider-speech'
|
||||
export const OFFICIAL_SPEECH_STREAMING_PROVIDER_ID = 'official-provider-speech-streaming'
|
||||
export const OFFICIAL_TRANSCRIPTION_PROVIDER_ID = 'official-provider-transcription'
|
||||
export const OFFICIAL_VISION_PROVIDER_ID = 'vision-official-provider'
|
||||
export { OFFICIAL_CHAT_PROVIDER_ID, OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID, OFFICIAL_TRANSCRIPTION_PROVIDER_ID, OFFICIAL_VISION_PROVIDER_ID } from './constants'
|
||||
|
||||
// Locale → voice id map recommended by the server, keyed by provider id.
|
||||
// Populated by each speech provider's listVoices() from the response's
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AiriExtension } from '../types/airiCard'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { applyAiriCardEditorModules, safeParseAiriCardDraft } from './airi-card-editor'
|
||||
import { applyAiriCardEditorModules, getAiriCardEditorModuleSettings, safeParseAiriCardDraft } from './airi-card-editor'
|
||||
|
||||
describe('airi card editor validation', () => {
|
||||
// https://github.com/moeru-ai/airi/issues/2108
|
||||
@@ -70,6 +70,56 @@ describe('airi card editor validation', () => {
|
||||
expect(result.output.artistryOptions).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps empty uploaded-card module fields inherited when saved', () => {
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The editor replaced empty uploaded-card fields with the current global
|
||||
// settings during initialization. Saving an unrelated change then stored
|
||||
// these values as card overrides.
|
||||
//
|
||||
// We fixed this by keeping empty module fields in the editor state. The
|
||||
// saved card keeps these fields empty, so they inherit global settings.
|
||||
const uploadedCard: Card = {
|
||||
...createCard(),
|
||||
description: 'Imported card',
|
||||
extensions: {
|
||||
airi: {
|
||||
modules: {
|
||||
consciousness: { provider: '', model: '' },
|
||||
vision: { provider: '', model: '' },
|
||||
speech: { provider: '', model: '', voice_id: '' },
|
||||
displayModelId: '',
|
||||
},
|
||||
agents: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const moduleSettings = getAiriCardEditorModuleSettings(uploadedCard)
|
||||
const savedCard = applyAiriCardEditorModules({
|
||||
...uploadedCard,
|
||||
description: 'Edited imported card',
|
||||
}, {
|
||||
...moduleSettings,
|
||||
artistry: {
|
||||
provider: '',
|
||||
model: '',
|
||||
promptPrefix: '',
|
||||
widgetInstruction: '',
|
||||
spawnMode: 'bg_widget',
|
||||
options: undefined,
|
||||
autonomousEnabled: false,
|
||||
autonomousThreshold: 70,
|
||||
},
|
||||
})
|
||||
|
||||
expect(savedCard.description).toBe('Edited imported card')
|
||||
expect(savedCard.extensions.airi.modules.consciousness).toEqual({ provider: '', model: '' })
|
||||
expect(savedCard.extensions.airi.modules.vision).toEqual({ provider: '', model: '' })
|
||||
expect(savedCard.extensions.airi.modules.speech).toMatchObject({ provider: '', model: '', voice_id: '' })
|
||||
expect(savedCard.extensions.airi.modules.displayModelId).toBe('')
|
||||
})
|
||||
|
||||
it('preserves AIRI extension fields that are not editable in the form', () => {
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
|
||||
@@ -37,6 +37,34 @@ interface AiriCardEditorModules {
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the module fields that the AIRI Card editor owns.
|
||||
*
|
||||
* Empty strings mean that the card inherits the corresponding global setting.
|
||||
*/
|
||||
export function getAiriCardEditorModuleSettings(
|
||||
card: Card | undefined,
|
||||
): Pick<AiriCardEditorModules, 'consciousness' | 'vision' | 'speech' | 'displayModelId'> {
|
||||
const modules = getAiriCardModules(card?.extensions?.airi)
|
||||
|
||||
return {
|
||||
consciousness: {
|
||||
provider: getModuleString(modules?.consciousness, 'provider'),
|
||||
model: getModuleString(modules?.consciousness, 'model'),
|
||||
},
|
||||
vision: {
|
||||
provider: getModuleString(modules?.vision, 'provider'),
|
||||
model: getModuleString(modules?.vision, 'model'),
|
||||
},
|
||||
speech: {
|
||||
provider: getModuleString(modules?.speech, 'provider'),
|
||||
model: getModuleString(modules?.speech, 'model'),
|
||||
voice_id: getModuleString(modules?.speech, 'voice_id'),
|
||||
},
|
||||
displayModelId: getModuleString(modules, 'displayModelId'),
|
||||
}
|
||||
}
|
||||
|
||||
type CardWithAiriExtension = Card & {
|
||||
extensions: NonNullable<Card['extensions']> & {
|
||||
airi: AiriExtension
|
||||
@@ -159,6 +187,21 @@ function isAiriExtension(value: unknown): value is AiriExtension {
|
||||
&& isRecord(value.agents)
|
||||
}
|
||||
|
||||
function getAiriCardModules(value: unknown): Record<string, unknown> | undefined {
|
||||
if (!isRecord(value) || !isRecord(value.modules))
|
||||
return undefined
|
||||
|
||||
return value.modules
|
||||
}
|
||||
|
||||
function getModuleString(module: unknown, key: string): string {
|
||||
if (!isRecord(module))
|
||||
return ''
|
||||
|
||||
const value = module[key]
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { AiriExtension } from '../types/airiCard'
|
||||
|
||||
/** Persisted defaults, separate from the selections applied by the active card. */
|
||||
export type CardModuleDefaults = Pick<AiriExtension['modules'], 'consciousness' | 'vision' | 'speech' | 'displayModelId'>
|
||||
|
||||
/**
|
||||
* Resolves a card selection without borrowing a model from another provider.
|
||||
* An empty model remains unconfigured when the card changes provider.
|
||||
* The caller must ask for a model or use that provider's own default.
|
||||
*/
|
||||
export function resolveModuleSelection(
|
||||
selection: AiriExtension['modules']['consciousness'],
|
||||
defaults: AiriExtension['modules']['consciousness'],
|
||||
): AiriExtension['modules']['consciousness'] {
|
||||
const provider = selection.provider || defaults.provider
|
||||
const model = selection.model || (provider === defaults.provider ? defaults.model : '')
|
||||
return { provider, model }
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { SyncedPiniaRuntime } from 'pinia-plugin-synced'
|
||||
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
import { createPinia, disposePinia, setActivePinia } from 'pinia'
|
||||
import { createSyncedPiniaPlugin } from 'pinia-plugin-synced'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import { useAiriCardStore } from './airi-card'
|
||||
import { useConsciousnessStore } from './consciousness'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ locale: { value: 'en' }, t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
const runtimes: SyncedPiniaRuntime[] = []
|
||||
const piniaInstances: ReturnType<typeof createPinia>[] = []
|
||||
|
||||
function createContext(runtime?: SyncedPiniaRuntime) {
|
||||
const pinia = createPinia()
|
||||
if (runtime) {
|
||||
runtimes.push(runtime)
|
||||
pinia.use(runtime.plugin)
|
||||
}
|
||||
createApp({}).use(pinia).use(PiniaColada)
|
||||
setActivePinia(pinia)
|
||||
piniaInstances.push(pinia)
|
||||
return { pinia, cards: useAiriCardStore(pinia), consciousness: useConsciousnessStore(pinia) }
|
||||
}
|
||||
|
||||
describe('persisted and replicated card defaults', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ voices: [], recommended: {}, data: [] }))))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const runtime of runtimes.splice(0))
|
||||
runtime.dispose()
|
||||
for (const pinia of piniaInstances.splice(0))
|
||||
disposePinia(pinia)
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
// https://github.com/moeru-ai/airi/pull/2332
|
||||
// ROOT CAUSE:
|
||||
// A null storage default selects VueUse's string serializer. After reload,
|
||||
// the editor received text instead of module settings. Specify JSON storage.
|
||||
it('reads saved defaults as settings after a reload', async () => {
|
||||
localStorage.setItem('airi-card-module-defaults', JSON.stringify({
|
||||
consciousness: { provider: 'ollama', model: 'global-model' },
|
||||
vision: { provider: '', model: '' },
|
||||
speech: { provider: 'speech-noop', model: '', voice_id: '' },
|
||||
displayModelId: 'preset-live2d-1',
|
||||
}))
|
||||
const { cards, consciousness } = createContext()
|
||||
expect(cards.moduleDefaults?.consciousness.provider).toBe('ollama')
|
||||
await cards.initialize()
|
||||
expect(consciousness.activeModel).toBe('global-model')
|
||||
})
|
||||
|
||||
it('does not publish a follower state proposal after a card snapshot', async () => {
|
||||
const namespace = `card-inheritance-${crypto.randomUUID()}`
|
||||
const onError = vi.fn()
|
||||
const leaderRuntime = createSyncedPiniaPlugin({ namespace, leadership: 'leader-only', onError })
|
||||
const leader = createContext(leaderRuntime)
|
||||
await expect.poll(() => leaderRuntime.isLeader()).toBe(true)
|
||||
leader.consciousness.activeProvider = 'ollama'
|
||||
leader.consciousness.activeModel = 'global-model'
|
||||
await leader.cards.initialize()
|
||||
|
||||
const followerRuntime = createSyncedPiniaPlugin({ namespace, leadership: 'follower-only', onError })
|
||||
const follower = createContext(followerRuntime)
|
||||
await expect.poll(() => follower.cards.moduleDefaults?.consciousness.model).toBe('global-model')
|
||||
await expect.poll(() => follower.consciousness.activeModel).toBe('global-model')
|
||||
const traffic = vi.spyOn(BroadcastChannel.prototype, 'postMessage')
|
||||
await leader.cards.updateCard('default', {
|
||||
...leader.cards.activeCard!,
|
||||
description: 'Changed in another window',
|
||||
})
|
||||
await expect.poll(() => follower.cards.activeCard?.description).toBe('Changed in another window')
|
||||
await nextTick()
|
||||
expect(follower.consciousness.activeModel).toBe('global-model')
|
||||
// replaceState is the plugin's follower-to-leader proposal RPC. A received
|
||||
// snapshot must not call it, even when Vue watchers observe that snapshot.
|
||||
const proposals = traffic.mock.calls.filter(([message]) => JSON.stringify(message).includes('replaceState'))
|
||||
expect(proposals).toHaveLength(0)
|
||||
|
||||
const explicit = await leader.cards.addCard({
|
||||
...leader.cards.activeCard!,
|
||||
extensions: {
|
||||
airi: {
|
||||
agents: {},
|
||||
modules: {
|
||||
consciousness: { provider: 'another-provider', model: 'card-model' },
|
||||
vision: { provider: '', model: '' },
|
||||
speech: { provider: '', model: '', voice_id: '' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}, 'import')
|
||||
await leader.cards.activateCard(explicit)
|
||||
await expect.poll(() => follower.consciousness.activeModel).toBe('card-model')
|
||||
await leader.cards.activateCard('default')
|
||||
await expect.poll(() => follower.consciousness.activeModel).toBe('global-model')
|
||||
expect(follower.cards.moduleDefaults?.consciousness.model).toBe('global-model')
|
||||
expect(traffic.mock.calls.filter(([message]) => JSON.stringify(message).includes('replaceState'))).toHaveLength(0)
|
||||
expect(onError).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,298 @@
|
||||
import type { AiriCard } from '../../types/airiCard'
|
||||
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, nextTick, toRaw } from 'vue'
|
||||
|
||||
import { DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT } from '../../constants/prompts/character-defaults'
|
||||
import { useAiriCardStore } from './airi-card'
|
||||
import { useArtistryStore } from './artistry'
|
||||
import { useConsciousnessStore } from './consciousness'
|
||||
import { useSpeechStore } from './speech'
|
||||
import { useVisionStore } from './vision'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ locale: { value: 'en' }, t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
vi.mock('localforage', () => ({
|
||||
default: {
|
||||
getItem: vi.fn(async () => undefined),
|
||||
iterate: vi.fn(async () => undefined),
|
||||
removeItem: vi.fn(async () => undefined),
|
||||
setItem: vi.fn(async <T>(_: string, value: T) => value),
|
||||
},
|
||||
}))
|
||||
|
||||
function card(provider = '', model = ''): AiriCard {
|
||||
return {
|
||||
name: 'Imported card',
|
||||
version: '1.0.0',
|
||||
description: '',
|
||||
extensions: {
|
||||
airi: {
|
||||
modules: {
|
||||
consciousness: { provider, model },
|
||||
vision: { provider: '', model: '' },
|
||||
speech: { provider: '', model: '', voice_id: '' },
|
||||
},
|
||||
agents: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('card inheritance with real module stores', () => {
|
||||
beforeEach(() => {
|
||||
const pinia = createPinia()
|
||||
createApp({}).use(pinia).use(PiniaColada)
|
||||
setActivePinia(pinia)
|
||||
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ recommended: {}, voices: [], data: [] }))))
|
||||
})
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
// https://github.com/moeru-ai/airi/pull/2332
|
||||
// ROOT CAUSE:
|
||||
// Empty fields skipped runtime writes, so the previous card became the default.
|
||||
// Resolve each activation from saved defaults instead of the previous runtime.
|
||||
it('restores global settings after an explicit card', async () => {
|
||||
const consciousness = useConsciousnessStore()
|
||||
consciousness.activeProvider = 'global-provider'
|
||||
consciousness.activeModel = 'global-model'
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const explicit = await cards.addCard(card('card-provider', 'card-model'), 'import')
|
||||
const inherited = await cards.addCard(card(), 'import')
|
||||
await cards.activateCard(explicit)
|
||||
expect(consciousness.activeModel).toBe('card-model')
|
||||
await cards.activateCard(inherited)
|
||||
expect(consciousness.activeProvider).toBe('global-provider')
|
||||
expect(consciousness.activeModel).toBe('global-model')
|
||||
})
|
||||
|
||||
// https://github.com/moeru-ai/airi/pull/2332
|
||||
it('does not inherit a model from a different provider', async () => {
|
||||
const vision = useVisionStore()
|
||||
vision.activeProvider = 'global-provider'
|
||||
vision.activeModel = 'global-model'
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const uploaded = card()
|
||||
uploaded.extensions.airi.modules.vision.provider = 'another-provider'
|
||||
const id = await cards.addCard(uploaded, 'import')
|
||||
await cards.activateCard(id)
|
||||
expect(vision.activeProvider).toBe('another-provider')
|
||||
expect(vision.activeModel).toBe('')
|
||||
})
|
||||
|
||||
// https://github.com/moeru-ai/airi/pull/2332
|
||||
it('preserves explicit no-speech on the default card', async () => {
|
||||
const cards = useAiriCardStore()
|
||||
const muted = card()
|
||||
muted.extensions.airi.modules.speech.provider = 'speech-noop'
|
||||
cards.cards.set('default', muted)
|
||||
await cards.initialize()
|
||||
expect(cards.activeCard?.extensions.airi.modules.speech.provider).toBe('speech-noop')
|
||||
expect(useSpeechStore().activeSpeechProvider).toBe('speech-noop')
|
||||
})
|
||||
|
||||
it('keeps the default artistry instruction on a fresh profile', async () => {
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
expect(cards.activeCard?.extensions.airi.modules.artistry?.widgetInstruction).toBe(DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT)
|
||||
expect(cards.systemPrompt).toContain(DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT)
|
||||
})
|
||||
|
||||
it('restores defaults when an active override is cleared in the editor', async () => {
|
||||
const consciousness = useConsciousnessStore()
|
||||
consciousness.activeProvider = 'global-provider'
|
||||
consciousness.activeModel = 'global-model'
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const id = await cards.addCard(card('card-provider', 'card-model'), 'import')
|
||||
await cards.activateCard(id)
|
||||
await cards.updateCard(id, card())
|
||||
expect(consciousness.activeProvider).toBe('global-provider')
|
||||
expect(consciousness.activeModel).toBe('global-model')
|
||||
})
|
||||
|
||||
it('inherits a model only within the same provider', async () => {
|
||||
const consciousness = useConsciousnessStore()
|
||||
consciousness.activeProvider = 'global-provider'
|
||||
consciousness.activeModel = 'global-model'
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const id = await cards.addCard(card('global-provider'), 'import')
|
||||
await cards.activateCard(id)
|
||||
expect(consciousness.activeModel).toBe('global-model')
|
||||
expect(cards.activeCard?.extensions.airi.modules.consciousness.model).toBe('')
|
||||
})
|
||||
|
||||
it('keeps imported cards without an AIRI extension inherited', async () => {
|
||||
const consciousness = useConsciousnessStore()
|
||||
consciousness.activeProvider = 'global-provider'
|
||||
consciousness.activeModel = 'global-model'
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const id = await cards.addCard({ name: 'Portable card', version: '1.0.0', description: '' }, 'import')
|
||||
expect(cards.getCard(id)?.extensions.airi.modules.consciousness).toEqual({ provider: '', model: '' })
|
||||
})
|
||||
|
||||
it('preserves imported extension fields while filling module defaults', async () => {
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const uploaded = card()
|
||||
const extensions = {
|
||||
other: { theme: 'blue' },
|
||||
airi: {
|
||||
...uploaded.extensions.airi,
|
||||
importedMetadata: { revision: 2 },
|
||||
modules: {
|
||||
...uploaded.extensions.airi.modules,
|
||||
customModule: { enabled: true },
|
||||
speech: { ...uploaded.extensions.airi.modules.speech, customVoiceOptions: { volume: 0.7 }, rate: 1.2 },
|
||||
},
|
||||
},
|
||||
}
|
||||
const id = await cards.addCard({ ...uploaded, extensions }, 'import')
|
||||
expect(cards.getCard(id)?.extensions).toMatchObject(extensions)
|
||||
})
|
||||
|
||||
it('does not restore a removed provider from saved defaults', async () => {
|
||||
const consciousness = useConsciousnessStore()
|
||||
consciousness.activeProvider = 'removed-provider'
|
||||
consciousness.activeModel = 'global-model'
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const id = await cards.addCard(card('removed-provider', 'card-model'), 'import')
|
||||
await cards.activateCard(id)
|
||||
await cards.clearProviderSelections('removed-provider')
|
||||
expect(cards.moduleDefaults?.consciousness).toEqual({ provider: '', model: '' })
|
||||
expect(cards.getCard(id)?.extensions.airi.modules.consciousness).toEqual({ provider: '', model: '' })
|
||||
await cards.activateCard('default')
|
||||
expect(consciousness.activeProvider).toBe('')
|
||||
expect(consciousness.activeModel).toBe('')
|
||||
})
|
||||
|
||||
it('does not inherit a voice from a different speech model', async () => {
|
||||
const speech = useSpeechStore()
|
||||
speech.activeSpeechProvider = 'speech-provider'
|
||||
speech.activeSpeechModel = 'global-model'
|
||||
speech.activeSpeechVoiceId = 'global-voice'
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const uploaded = card()
|
||||
uploaded.extensions.airi.modules.speech.model = 'card-model'
|
||||
const id = await cards.addCard(uploaded, 'import')
|
||||
await cards.activateCard(id)
|
||||
expect(speech.activeSpeechModel).toBe('card-model')
|
||||
expect(speech.activeSpeechVoiceId).toBe('')
|
||||
await cards.activateCard('default')
|
||||
expect(speech.activeSpeechModel).toBe('global-model')
|
||||
expect(speech.activeSpeechVoiceId).toBe('global-voice')
|
||||
})
|
||||
|
||||
it('keeps artistry models and options within their provider', async () => {
|
||||
const artistry = useArtistryStore()
|
||||
artistry.globalProvider = 'comfyui'
|
||||
artistry.globalModel = 'global-model'
|
||||
artistry.globalProviderOptions = { workflow: 'global-workflow' }
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const uploaded = card()
|
||||
uploaded.extensions.airi.modules.artistry = { enabled: true, provider: 'replicate', model: '' }
|
||||
const id = await cards.addCard(uploaded, 'import')
|
||||
await cards.activateCard(id)
|
||||
expect(artistry.activeProvider).toBe('replicate')
|
||||
expect(artistry.activeModel).toBe('')
|
||||
expect(artistry.providerOptions).toBeUndefined()
|
||||
await cards.activateCard('default')
|
||||
expect(artistry.activeModel).toBe('global-model')
|
||||
expect(artistry.providerOptions).toEqual({ workflow: 'global-workflow' })
|
||||
})
|
||||
|
||||
it('keeps defaults separate across persisted state and follower snapshots', async () => {
|
||||
const leader = createPinia()
|
||||
createApp({}).use(leader).use(PiniaColada)
|
||||
setActivePinia(leader)
|
||||
const consciousness = useConsciousnessStore(leader)
|
||||
consciousness.activeProvider = 'global-provider'
|
||||
consciousness.activeModel = 'global-model'
|
||||
const cards = useAiriCardStore(leader)
|
||||
await cards.initialize()
|
||||
const id = await cards.addCard(card('card-provider', 'card-model'), 'import')
|
||||
await cards.activateCard(id)
|
||||
const snapshot = structuredClone({
|
||||
cards: toRaw(cards.cards),
|
||||
activeCardId: cards.activeCardId,
|
||||
moduleDefaults: toRaw(cards.moduleDefaults),
|
||||
})
|
||||
|
||||
const follower = createPinia()
|
||||
createApp({}).use(follower).use(PiniaColada)
|
||||
setActivePinia(follower)
|
||||
const followerConsciousness = useConsciousnessStore(follower)
|
||||
followerConsciousness.activeProvider = 'card-provider'
|
||||
followerConsciousness.activeModel = 'card-model'
|
||||
const followerCards = useAiriCardStore(follower)
|
||||
const updates = vi.fn()
|
||||
followerConsciousness.$subscribe(updates, { flush: 'sync' })
|
||||
followerCards.$patch(snapshot)
|
||||
await nextTick()
|
||||
// Applying a card snapshot must not start another runtime transition.
|
||||
expect(updates).not.toHaveBeenCalled()
|
||||
await followerCards.initialize()
|
||||
await followerCards.activateCard('default')
|
||||
expect(followerConsciousness.activeProvider).toBe('global-provider')
|
||||
expect(followerConsciousness.activeModel).toBe('global-model')
|
||||
})
|
||||
|
||||
it('keeps card overrides unchanged across login, logout, and another activation', async () => {
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const id = await cards.addCard(card('official-provider', 'auto'), 'import')
|
||||
await cards.activateCard(id)
|
||||
await cards.configureForAuthentication(true)
|
||||
expect(useConsciousnessStore().activeProvider).toBe('official-provider')
|
||||
await cards.configureForAuthentication(false)
|
||||
expect(useConsciousnessStore().activeProvider).toBe('')
|
||||
await cards.activateCard('default')
|
||||
await cards.activateCard(id)
|
||||
expect(useConsciousnessStore().activeProvider).toBe('')
|
||||
expect(cards.activeCard?.extensions.airi.modules.consciousness).toEqual({ provider: 'official-provider', model: 'auto' })
|
||||
await cards.configureForAuthentication(true)
|
||||
expect(useConsciousnessStore().activeModel).toBe('auto')
|
||||
})
|
||||
|
||||
it('does not write login defaults into empty uploaded-card fields', async () => {
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const id = await cards.addCard(card(), 'import')
|
||||
await cards.activateCard(id)
|
||||
await cards.configureForAuthentication(true)
|
||||
expect(useConsciousnessStore().activeModel).toBe('auto')
|
||||
expect(cards.activeCard?.extensions.airi.modules.consciousness).toEqual({ provider: '', model: '' })
|
||||
await cards.configureForAuthentication(false)
|
||||
expect(cards.activeCard?.extensions.airi.modules.speech.provider).toBe('')
|
||||
expect(useSpeechStore().activeSpeechProvider).toBe('speech-noop')
|
||||
})
|
||||
|
||||
// https://github.com/moeru-ai/airi/pull/2332#discussion_r3820590225
|
||||
it('reapplies model and voice overrides after login fills inherited providers', async () => {
|
||||
const cards = useAiriCardStore()
|
||||
await cards.initialize()
|
||||
const uploaded = card('', 'card-model')
|
||||
uploaded.extensions.airi.modules.speech.voice_id = 'card-voice'
|
||||
const id = await cards.addCard(uploaded, 'import')
|
||||
await cards.activateCard(id)
|
||||
await cards.configureForAuthentication(true)
|
||||
expect(useConsciousnessStore().activeProvider).toBe('official-provider')
|
||||
expect(useConsciousnessStore().activeModel).toBe('card-model')
|
||||
expect(useSpeechStore().activeSpeechVoiceId).toBe('card-voice')
|
||||
expect(cards.moduleDefaults?.consciousness.model).toBe('auto')
|
||||
expect(cards.activeCard?.extensions.airi.modules.consciousness.provider).toBe('')
|
||||
expect(cards.activeCard?.extensions.airi.modules.speech.provider).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -130,6 +130,87 @@ describe('airi-card store', () => {
|
||||
expect(visionStore.activeModel).toBe('mock-vision-model')
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The default card took a snapshot before sign-in. Its speech provider was
|
||||
// `speech-noop`, so a later activation replaced the official speech provider.
|
||||
// The card then showed missing module settings instead of inherited settings.
|
||||
//
|
||||
// We fix this by keeping the default card module fields empty. Empty fields
|
||||
// inherit the global module settings and never replace them during activation.
|
||||
it('keeps default card modules inherited after authenticated defaults load', async () => {
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const visionStore = useVisionStore()
|
||||
const cardStore = useAiriCardStore()
|
||||
|
||||
consciousnessStore.activeProvider = ''
|
||||
consciousnessStore.activeModel = ''
|
||||
speechStore.activeSpeechProvider = 'speech-noop'
|
||||
speechStore.activeSpeechModel = ''
|
||||
speechStore.activeSpeechVoiceId = ''
|
||||
visionStore.activeProvider = ''
|
||||
visionStore.activeModel = ''
|
||||
|
||||
await cardStore.initialize()
|
||||
|
||||
consciousnessStore.activeProvider = 'official-provider'
|
||||
consciousnessStore.activeModel = 'auto'
|
||||
speechStore.activeSpeechProvider = 'official-provider-speech'
|
||||
speechStore.activeSpeechModel = 'auto'
|
||||
visionStore.activeProvider = 'vision-official-provider'
|
||||
visionStore.activeModel = 'auto'
|
||||
|
||||
await cardStore.activateCard('default')
|
||||
|
||||
expect(cardStore.activeCard?.extensions.airi.modules.consciousness).toEqual({
|
||||
provider: '',
|
||||
model: '',
|
||||
})
|
||||
expect(cardStore.activeCard?.extensions.airi.modules.speech).toMatchObject({
|
||||
provider: '',
|
||||
model: '',
|
||||
voice_id: '',
|
||||
})
|
||||
expect(cardStore.activeCard?.extensions.airi.modules.vision).toEqual({
|
||||
provider: '',
|
||||
model: '',
|
||||
})
|
||||
expect(consciousnessStore.activeProvider).toBe('official-provider')
|
||||
expect(consciousnessStore.activeModel).toBe('auto')
|
||||
expect(speechStore.activeSpeechProvider).toBe('official-provider-speech')
|
||||
expect(speechStore.activeSpeechModel).toBe('auto')
|
||||
expect(visionStore.activeProvider).toBe('vision-official-provider')
|
||||
expect(visionStore.activeModel).toBe('auto')
|
||||
})
|
||||
|
||||
it('preserves ambiguous old default speech settings', async () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.cards.set('default', {
|
||||
name: 'ReLU',
|
||||
version: '1.0.0',
|
||||
description: 'Built-in card from before provider defaults loaded.',
|
||||
extensions: {
|
||||
airi: {
|
||||
modules: {
|
||||
consciousness: { provider: '', model: '' },
|
||||
speech: { provider: 'speech-noop', model: '', voice_id: '' },
|
||||
vision: { provider: '', model: '' },
|
||||
},
|
||||
agents: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await cardStore.initialize()
|
||||
|
||||
expect(cardStore.activeCard?.extensions.airi.modules.speech).toMatchObject({
|
||||
provider: 'speech-noop',
|
||||
model: '',
|
||||
voice_id: '',
|
||||
})
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// Each Electron window called the synchronized initialize action. The leader
|
||||
@@ -161,50 +242,6 @@ describe('airi-card store', () => {
|
||||
expect(visionStore.activeModel).toBe('auto')
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The authentication hook updated the runtime module stores, but the active
|
||||
// card kept its older empty selections. A later card activation restored
|
||||
// speech-noop and erased the authenticated defaults.
|
||||
//
|
||||
// We fixed this by persisting the resolved runtime selections in one card
|
||||
// command without applying the card back to the runtime.
|
||||
it('persists runtime module selections without reapplying the active card', async () => {
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const visionStore = useVisionStore()
|
||||
const cardStore = useAiriCardStore()
|
||||
await cardStore.initialize()
|
||||
resetArtistryToGlobal.mockClear()
|
||||
|
||||
consciousnessStore.activeProvider = 'official-provider'
|
||||
consciousnessStore.activeModel = 'auto'
|
||||
speechStore.activeSpeechProvider = 'official-provider-speech'
|
||||
speechStore.activeSpeechModel = 'auto'
|
||||
speechStore.activeSpeechVoiceId = ''
|
||||
visionStore.activeProvider = 'vision-official-provider'
|
||||
visionStore.activeModel = 'auto'
|
||||
|
||||
await expect(cardStore.persistActiveCardModuleSelections()).resolves.toBe(true)
|
||||
|
||||
expect(cardStore.activeCard?.extensions.airi.modules.consciousness).toEqual({
|
||||
provider: 'official-provider',
|
||||
model: 'auto',
|
||||
})
|
||||
expect(cardStore.activeCard?.extensions.airi.modules.speech).toMatchObject({
|
||||
provider: 'official-provider-speech',
|
||||
model: 'auto',
|
||||
voice_id: '',
|
||||
})
|
||||
expect(cardStore.activeCard?.extensions.airi.modules.vision).toEqual({
|
||||
provider: 'vision-official-provider',
|
||||
model: 'auto',
|
||||
})
|
||||
expect(resetArtistryToGlobal).not.toHaveBeenCalled()
|
||||
|
||||
await expect(cardStore.persistActiveCardModuleSelections()).resolves.toBe(false)
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// A synchronized state snapshot replaced `activeCardId`. The old watcher
|
||||
@@ -289,17 +326,17 @@ describe('airi-card store', () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
await cardStore.initialize()
|
||||
|
||||
expect(await cardStore.updateActiveCardDisplayModel('display-model-iru-v2')).toBe(true)
|
||||
expect(await cardStore.updateActiveCardDisplayModel('preset-vrm-1')).toBe(true)
|
||||
expect(await cardStore.updateActiveCardConsciousness({ provider: 'openrouter-ai', model: 'anthropic/claude-sonnet' })).toBe(true)
|
||||
expect(await cardStore.updateActiveCardVision({ provider: 'ollama', model: 'llava' })).toBe(true)
|
||||
expect(await cardStore.updateActiveCardSpeech({ provider: 'elevenlabs', model: 'eleven_multilingual_v2', voice_id: 'aria' })).toBe(true)
|
||||
expect(cardStore.activeCard?.extensions.airi.modules).toMatchObject({
|
||||
displayModelId: 'display-model-iru-v2',
|
||||
displayModelId: 'preset-vrm-1',
|
||||
consciousness: { provider: 'openrouter-ai', model: 'anthropic/claude-sonnet' },
|
||||
vision: { provider: 'ollama', model: 'llava' },
|
||||
speech: { provider: 'elevenlabs', model: 'eleven_multilingual_v2', voice_id: 'aria' },
|
||||
})
|
||||
expect(stageModelStore.stageModelSelected).toBe('display-model-iru-v2')
|
||||
expect(stageModelStore.stageModelSelected).toBe('preset-vrm-1')
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import type { Card, ccv3 } from '@proj-airi/ccc'
|
||||
|
||||
import type { CardModuleDefaults } from '../../services/airi-card-modules'
|
||||
import type { AiriCard, AiriExtension } from '../../types/airiCard'
|
||||
|
||||
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
|
||||
import { StorageSerializers } from '@vueuse/core'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { computed, toRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT } from '../../constants/prompts/character-defaults'
|
||||
import { captureAnalyticsEvent } from '../../libs/product-signals'
|
||||
import { resolveModuleSelection } from '../../services/airi-card-modules'
|
||||
import { useProviderConfigStore } from '../providers/config'
|
||||
import { useSettingsStageModel } from '../settings/stage-model'
|
||||
import { useArtistryStore } from './artistry'
|
||||
import { useConsciousnessStore } from './consciousness'
|
||||
import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from './default'
|
||||
import { useSpeechStore } from './speech'
|
||||
import { useVisionStore } from './vision'
|
||||
|
||||
@@ -45,6 +50,16 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
const activeCardId = useLocalStorageManualReset<string>('airi-card-active-id', 'default', { listenToStorageChanges: false })
|
||||
let initialized = false
|
||||
|
||||
// Only leader-owned commands change defaults or apply card overrides. Runtime
|
||||
// module stores contain the effective selections, not another source of defaults.
|
||||
// Existing installations seed this snapshot once from their current settings.
|
||||
const moduleDefaults = useLocalStorageManualReset<CardModuleDefaults | null>('airi-card-module-defaults', null, {
|
||||
listenToStorageChanges: false,
|
||||
serializer: StorageSerializers.object,
|
||||
})
|
||||
let appliedModules: AiriExtension['modules'] | undefined
|
||||
let pendingAuthenticationSetup: Promise<void> | undefined
|
||||
|
||||
const activeCard = computed(() => cards.value.get(activeCardId.value))
|
||||
function useRuntimeModuleStores() {
|
||||
return {
|
||||
@@ -56,6 +71,102 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function readRuntimeModules(): CardModuleDefaults {
|
||||
const { consciousness, vision, speech, stageModel } = useRuntimeModuleStores()
|
||||
return {
|
||||
consciousness: { provider: consciousness.activeProvider, model: consciousness.activeModel },
|
||||
vision: { provider: vision.activeProvider, model: vision.activeModel },
|
||||
speech: { provider: speech.activeSpeechProvider, model: speech.activeSpeechModel, voice_id: speech.activeSpeechVoiceId },
|
||||
displayModelId: stageModel.stageModelSelected,
|
||||
}
|
||||
}
|
||||
|
||||
function rememberInheritedSettings() {
|
||||
const runtime = readRuntimeModules()
|
||||
const defaults = moduleDefaults.value
|
||||
if (!defaults) {
|
||||
moduleDefaults.value = runtime
|
||||
return
|
||||
}
|
||||
if (!appliedModules)
|
||||
return
|
||||
|
||||
// A user can change an inherited setting through a module surface. Retain
|
||||
// those changes, but never promote the previous card's overrides to defaults.
|
||||
const next = structuredClone(toRaw(defaults))
|
||||
for (const module of ['consciousness', 'vision', 'speech'] as const) {
|
||||
const previous = appliedModules[module]
|
||||
if (previous.provider)
|
||||
continue
|
||||
if (next[module].provider !== runtime[module].provider)
|
||||
next[module].model = ''
|
||||
next[module].provider = runtime[module].provider
|
||||
if (!previous.model)
|
||||
next[module].model = runtime[module].model
|
||||
}
|
||||
if (!appliedModules.speech.provider && !appliedModules.speech.model && !appliedModules.speech.voice_id)
|
||||
next.speech.voice_id = runtime.speech.voice_id
|
||||
if (!appliedModules.displayModelId)
|
||||
next.displayModelId = runtime.displayModelId
|
||||
moduleDefaults.value = next
|
||||
}
|
||||
|
||||
function writeRuntimeModules(modules: CardModuleDefaults) {
|
||||
const { consciousness, vision, speech, stageModel } = useRuntimeModuleStores()
|
||||
// Provider changes synchronously clear dependent selections. Assign the
|
||||
// resolved model and voice afterwards, including empty values.
|
||||
consciousness.activeProvider = modules.consciousness.provider
|
||||
consciousness.activeModel = modules.consciousness.model
|
||||
vision.activeProvider = modules.vision.provider
|
||||
vision.activeModel = modules.vision.model
|
||||
speech.activeSpeechProvider = modules.speech.provider
|
||||
speech.activeSpeechModel = modules.speech.model
|
||||
speech.activeSpeechVoiceId = modules.speech.voice_id
|
||||
if (modules.displayModelId !== undefined)
|
||||
stageModel.stageModelSelected = modules.displayModelId
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates authenticated defaults without changing any card's stored overrides.
|
||||
* The synchronization leader owns provider setup, persistence, and reapplication.
|
||||
*/
|
||||
async function configureForAuthentication(authenticated: boolean) {
|
||||
const previous = pendingAuthenticationSetup
|
||||
const operation = (async () => {
|
||||
// A failed setup is reported to its caller. The next auth event must
|
||||
// still run, for example to remove providers after a failed login.
|
||||
if (previous)
|
||||
await previous.catch(() => {})
|
||||
await applyAuthenticationDefaults(authenticated)
|
||||
})()
|
||||
pendingAuthenticationSetup = operation
|
||||
try {
|
||||
await operation
|
||||
}
|
||||
finally {
|
||||
if (pendingAuthenticationSetup === operation)
|
||||
pendingAuthenticationSetup = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function applyAuthenticationDefaults(authenticated: boolean) {
|
||||
rememberInheritedSettings()
|
||||
if (!moduleDefaults.value)
|
||||
return
|
||||
writeRuntimeModules(moduleDefaults.value)
|
||||
try {
|
||||
if (authenticated)
|
||||
await configureAsDefaultsIfEmpty()
|
||||
else
|
||||
await unconfigureAuthenticationProviders()
|
||||
moduleDefaults.value = readRuntimeModules()
|
||||
}
|
||||
finally {
|
||||
appliedModules = undefined
|
||||
applyActiveCardSettings()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `source` feeds the `card_created` analytics event: `scratch` = built in
|
||||
* the creation dialog, `import` = ccv3 JSON upload, `duplicate` = cloned
|
||||
@@ -70,6 +181,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
|
||||
const removeCard = async (id: string) => {
|
||||
await pendingAuthenticationSetup
|
||||
// The built-in card is the guaranteed fallback for every runtime profile.
|
||||
if (id === 'default')
|
||||
return false
|
||||
@@ -90,6 +202,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
|
||||
const updateCard = async (id: string, updates: AiriCard | Card | ccv3.CharacterCardV3) => {
|
||||
await pendingAuthenticationSetup
|
||||
const existingCard = cards.value.get(id)
|
||||
if (!existingCard)
|
||||
return false
|
||||
@@ -136,6 +249,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
|
||||
async function updateActiveCardDisplayModel(displayModelId: string | undefined) {
|
||||
await pendingAuthenticationSetup
|
||||
const updated = updateActiveCardModules(() => ({ displayModelId }))
|
||||
if (updated)
|
||||
applyActiveCardSettings()
|
||||
@@ -143,6 +257,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
|
||||
async function updateActiveCardConsciousness(consciousness: AiriExtension['modules']['consciousness']) {
|
||||
await pendingAuthenticationSetup
|
||||
const updated = updateActiveCardModules(() => ({ consciousness }))
|
||||
if (updated)
|
||||
applyActiveCardSettings()
|
||||
@@ -150,6 +265,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
|
||||
async function updateActiveCardVision(vision: AiriExtension['modules']['vision']) {
|
||||
await pendingAuthenticationSetup
|
||||
const updated = updateActiveCardModules(() => ({ vision }))
|
||||
if (updated)
|
||||
applyActiveCardSettings()
|
||||
@@ -157,6 +273,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
|
||||
async function updateActiveCardSpeech(speech: Pick<AiriExtension['modules']['speech'], 'provider' | 'model' | 'voice_id'>) {
|
||||
await pendingAuthenticationSetup
|
||||
const updated = updateActiveCardModules(({ modules }) => ({
|
||||
speech: {
|
||||
...modules.speech,
|
||||
@@ -168,63 +285,33 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
return updated
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the current inference selections in the active card.
|
||||
*
|
||||
* This command snapshots runtime state after a higher-level operation, such
|
||||
* as authenticated default setup. It deliberately does not apply the card
|
||||
* back to the runtime, so one persistence write cannot start another module
|
||||
* transition.
|
||||
*/
|
||||
async function persistActiveCardModuleSelections() {
|
||||
const card = cards.value.get(activeCardId.value)
|
||||
if (!card)
|
||||
return false
|
||||
|
||||
const {
|
||||
consciousness,
|
||||
speech,
|
||||
vision,
|
||||
} = useRuntimeModuleStores()
|
||||
const modules = card.extensions?.airi?.modules
|
||||
const alreadyPersisted = modules?.consciousness?.provider === consciousness.activeProvider
|
||||
&& modules.consciousness.model === consciousness.activeModel
|
||||
&& modules?.speech?.provider === speech.activeSpeechProvider
|
||||
&& modules.speech.model === speech.activeSpeechModel
|
||||
&& modules.speech.voice_id === speech.activeSpeechVoiceId
|
||||
&& modules?.vision?.provider === vision.activeProvider
|
||||
&& modules.vision.model === vision.activeModel
|
||||
|
||||
if (alreadyPersisted)
|
||||
return false
|
||||
|
||||
return updateActiveCardModules(({ modules }) => ({
|
||||
consciousness: {
|
||||
provider: consciousness.activeProvider,
|
||||
model: consciousness.activeModel,
|
||||
},
|
||||
speech: {
|
||||
...modules.speech,
|
||||
provider: speech.activeSpeechProvider,
|
||||
model: speech.activeSpeechModel,
|
||||
voice_id: speech.activeSpeechVoiceId,
|
||||
},
|
||||
vision: {
|
||||
provider: vision.activeProvider,
|
||||
model: vision.activeModel,
|
||||
},
|
||||
/** Clears a removed provider from defaults and the active card, not other cards. */
|
||||
async function clearProviderSelections(providerId: string) {
|
||||
await pendingAuthenticationSetup
|
||||
rememberInheritedSettings()
|
||||
const defaults = moduleDefaults.value
|
||||
if (!defaults)
|
||||
return
|
||||
const next = structuredClone(toRaw(defaults))
|
||||
for (const module of ['consciousness', 'vision', 'speech'] as const) {
|
||||
if (next[module].provider === providerId) {
|
||||
next[module].provider = module === 'speech' ? 'speech-noop' : ''
|
||||
next[module].model = ''
|
||||
if (module === 'speech')
|
||||
next.speech.voice_id = ''
|
||||
}
|
||||
}
|
||||
moduleDefaults.value = next
|
||||
updateActiveCardModules(({ modules }) => ({
|
||||
consciousness: modules.consciousness.provider === providerId ? { provider: '', model: '' } : modules.consciousness,
|
||||
vision: modules.vision.provider === providerId ? { provider: '', model: '' } : modules.vision,
|
||||
speech: modules.speech.provider === providerId ? { ...modules.speech, provider: '', model: '', voice_id: '' } : modules.speech,
|
||||
}))
|
||||
appliedModules = undefined
|
||||
applyActiveCardSettings()
|
||||
}
|
||||
|
||||
function resolveAiriExtension(card: Card | ccv3.CharacterCardV3): AiriExtension {
|
||||
const {
|
||||
artistry,
|
||||
consciousness,
|
||||
speech,
|
||||
stageModel,
|
||||
vision,
|
||||
} = useRuntimeModuleStores()
|
||||
|
||||
// Get existing extension if available
|
||||
const existingExtension = ('data' in card
|
||||
? card.data?.extensions?.airi
|
||||
@@ -232,28 +319,18 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
|
||||
// Create default modules config
|
||||
const defaultModules = {
|
||||
consciousness: {
|
||||
provider: consciousness.activeProvider,
|
||||
model: consciousness.activeModel,
|
||||
},
|
||||
vision: {
|
||||
provider: vision.activeProvider,
|
||||
model: vision.activeModel,
|
||||
},
|
||||
speech: {
|
||||
provider: speech.activeSpeechProvider,
|
||||
model: speech.activeSpeechModel,
|
||||
voice_id: speech.activeSpeechVoiceId,
|
||||
},
|
||||
displayModelId: stageModel.stageModelSelected,
|
||||
consciousness: { provider: '', model: '' },
|
||||
vision: { provider: '', model: '' },
|
||||
speech: { provider: '', model: '', voice_id: '' },
|
||||
displayModelId: '',
|
||||
artistry: {
|
||||
enabled: false,
|
||||
provider: artistry.globalProvider,
|
||||
model: artistry.globalModel,
|
||||
promptPrefix: artistry.globalPromptPrefix,
|
||||
provider: '',
|
||||
model: '',
|
||||
promptPrefix: '',
|
||||
widgetInstruction: DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT,
|
||||
spawnMode: 'bg_widget' as const,
|
||||
options: artistry.globalProviderOptions,
|
||||
options: undefined,
|
||||
autonomousEnabled: false,
|
||||
autonomousThreshold: 70,
|
||||
autonomousTarget: 'assistant' as const,
|
||||
@@ -268,18 +345,23 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Merge existing extension with defaults
|
||||
// Fill known fields without discarding settings owned by imported extensions.
|
||||
return {
|
||||
...existingExtension,
|
||||
modules: {
|
||||
...existingExtension.modules,
|
||||
consciousness: {
|
||||
...existingExtension.modules?.consciousness,
|
||||
provider: existingExtension.modules?.consciousness?.provider ?? defaultModules.consciousness.provider,
|
||||
model: existingExtension.modules?.consciousness?.model ?? defaultModules.consciousness.model,
|
||||
},
|
||||
vision: {
|
||||
...existingExtension.modules?.vision,
|
||||
provider: existingExtension.modules?.vision?.provider ?? defaultModules.vision.provider,
|
||||
model: existingExtension.modules?.vision?.model ?? defaultModules.vision.model,
|
||||
},
|
||||
speech: {
|
||||
...existingExtension.modules?.speech,
|
||||
provider: existingExtension.modules?.speech?.provider ?? defaultModules.speech.provider,
|
||||
model: existingExtension.modules?.speech?.model ?? defaultModules.speech.model,
|
||||
voice_id: existingExtension.modules?.speech?.voice_id ?? defaultModules.speech.voice_id,
|
||||
@@ -293,6 +375,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
displayModelId: existingExtension.modules?.displayModelId ?? defaultModules.displayModelId,
|
||||
activeBackgroundId: existingExtension.modules?.activeBackgroundId,
|
||||
artistry: {
|
||||
...existingExtension.modules?.artistry,
|
||||
enabled: existingExtension.modules?.artistry?.enabled ?? (existingExtension as any).artistry?.enabled ?? defaultModules.artistry.enabled,
|
||||
provider: existingExtension.modules?.artistry?.provider ?? (existingExtension as any).artistry?.provider ?? defaultModules.artistry.provider,
|
||||
model: existingExtension.modules?.artistry?.model ?? (existingExtension as any).artistry?.model ?? defaultModules.artistry.model,
|
||||
@@ -343,8 +426,8 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
: [],
|
||||
tags: ccv3Card.data.tags ?? [],
|
||||
extensions: {
|
||||
airi: resolveAiriExtension(ccv3Card),
|
||||
...ccv3Card.data.extensions,
|
||||
airi: resolveAiriExtension(ccv3Card),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -352,13 +435,14 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
return {
|
||||
...card,
|
||||
extensions: {
|
||||
airi: resolveAiriExtension(card),
|
||||
...card.extensions,
|
||||
airi: resolveAiriExtension(card),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
await pendingAuthenticationSetup
|
||||
// This synchronized action executes in the leader. Each window calls it,
|
||||
// but only the first call can apply persisted card settings to the runtime.
|
||||
if (initialized)
|
||||
@@ -366,12 +450,26 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
|
||||
initialized = true
|
||||
if (!cards.value.has('default')) {
|
||||
cards.value.set('default', newAiriCard({
|
||||
const defaultCard: AiriCard = {
|
||||
name: 'ReLU',
|
||||
version: '1.0.0',
|
||||
description: t('base.prompt.prefix'),
|
||||
}))
|
||||
extensions: {
|
||||
airi: {
|
||||
modules: {
|
||||
consciousness: { provider: '', model: '' },
|
||||
speech: { provider: '', model: '', voice_id: '' },
|
||||
vision: { provider: '', model: '' },
|
||||
},
|
||||
agents: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
cards.value.set('default', newAiriCard(defaultCard))
|
||||
}
|
||||
|
||||
// Stored speech-noop can mean an intentional mute. Only the editor may
|
||||
// replace it with inheritance; the old placeholder has no provenance marker.
|
||||
// The active id and card map are persisted separately. Older versions
|
||||
// could delete the selected card without repairing its stored id.
|
||||
if (!cards.value.has(activeCardId.value))
|
||||
@@ -385,6 +483,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
* leader. Replicated state snapshots never invoke this command.
|
||||
*/
|
||||
async function activateCard(id: string) {
|
||||
await pendingAuthenticationSetup
|
||||
if (!cards.value.has(id))
|
||||
return false
|
||||
|
||||
@@ -394,13 +493,8 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
}
|
||||
|
||||
function applyActiveCardSettings(newCard = activeCard.value) {
|
||||
const {
|
||||
artistry,
|
||||
consciousness,
|
||||
speech,
|
||||
stageModel,
|
||||
vision,
|
||||
} = useRuntimeModuleStores()
|
||||
rememberInheritedSettings()
|
||||
const artistry = useArtistryStore()
|
||||
|
||||
artistry.resetToGlobal()
|
||||
|
||||
@@ -412,38 +506,47 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
if (!extension)
|
||||
return
|
||||
|
||||
const consciousnessSettings = extension.modules?.consciousness
|
||||
if (consciousnessSettings?.provider)
|
||||
consciousness.activeProvider = consciousnessSettings.provider
|
||||
if (consciousnessSettings?.model)
|
||||
consciousness.activeModel = consciousnessSettings.model
|
||||
|
||||
const visionSettings = extension.modules?.vision
|
||||
if (visionSettings?.provider)
|
||||
vision.activeProvider = visionSettings.provider
|
||||
if (visionSettings?.model)
|
||||
vision.activeModel = visionSettings.model
|
||||
|
||||
const speechSettings = extension.modules?.speech
|
||||
if (speechSettings?.provider)
|
||||
speech.activeSpeechProvider = speechSettings.provider
|
||||
if (speechSettings?.model)
|
||||
speech.activeSpeechModel = speechSettings.model
|
||||
if (speechSettings?.voice_id)
|
||||
speech.activeSpeechVoiceId = speechSettings.voice_id
|
||||
|
||||
// Apply body model if the card has a display model configured.
|
||||
// NOTICE: must set via store property directly (not storeToRefs .value) so Pinia's
|
||||
// proxy correctly calls the writable computed setter → stageModelSelectedState → updateStageModel().
|
||||
if (extension.modules?.displayModelId) {
|
||||
stageModel.stageModelSelected = extension.modules.displayModelId
|
||||
const defaults = moduleDefaults.value
|
||||
if (!defaults)
|
||||
return
|
||||
const modules = extension.modules
|
||||
const speechSelection = resolveModuleSelection(modules.speech, defaults.speech)
|
||||
const resolved: CardModuleDefaults = {
|
||||
consciousness: resolveModuleSelection(modules.consciousness, defaults.consciousness),
|
||||
vision: resolveModuleSelection(modules.vision, defaults.vision),
|
||||
speech: {
|
||||
...speechSelection,
|
||||
voice_id: modules.speech.voice_id || (
|
||||
speechSelection.provider === defaults.speech.provider && speechSelection.model === defaults.speech.model
|
||||
? defaults.speech.voice_id
|
||||
: ''
|
||||
),
|
||||
},
|
||||
displayModelId: modules.displayModelId || defaults.displayModelId,
|
||||
}
|
||||
const providers = useProviderConfigStore().providers
|
||||
for (const module of ['consciousness', 'vision', 'speech'] as const) {
|
||||
const provider = providers[resolved[module].provider]
|
||||
// Logout disables authenticated providers without deleting card choices.
|
||||
if (provider?.configuredBy === 'authentication' && provider.status === 'unconfigured') {
|
||||
resolved[module].provider = module === 'speech' ? 'speech-noop' : ''
|
||||
resolved[module].model = ''
|
||||
if (module === 'speech')
|
||||
resolved.speech.voice_id = ''
|
||||
}
|
||||
}
|
||||
writeRuntimeModules(resolved)
|
||||
appliedModules = modules
|
||||
|
||||
if (extension.modules?.artistry) {
|
||||
if (extension.modules.artistry.provider)
|
||||
artistry.activeProvider = extension.modules.artistry.provider
|
||||
if (extension.modules.artistry.model)
|
||||
artistry.activeModel = extension.modules.artistry.model
|
||||
const selection = resolveModuleSelection({
|
||||
provider: extension.modules.artistry.provider ?? '',
|
||||
model: extension.modules.artistry.model ?? '',
|
||||
}, { provider: artistry.globalProvider, model: artistry.globalModel })
|
||||
artistry.activeProvider = selection.provider
|
||||
artistry.activeModel = selection.model
|
||||
if (selection.provider !== artistry.globalProvider)
|
||||
artistry.providerOptions = undefined
|
||||
if (extension.modules.artistry.promptPrefix)
|
||||
artistry.defaultPromptPrefix = extension.modules.artistry.promptPrefix
|
||||
if (extension.modules.artistry.options)
|
||||
@@ -453,12 +556,15 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
|
||||
function resetState() {
|
||||
initialized = false
|
||||
appliedModules = undefined
|
||||
moduleDefaults.reset()
|
||||
cards.reset()
|
||||
activeCardId.reset()
|
||||
}
|
||||
|
||||
return {
|
||||
cards,
|
||||
moduleDefaults,
|
||||
activeCard,
|
||||
activeCardId,
|
||||
addCard,
|
||||
@@ -466,13 +572,14 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
updateCard,
|
||||
updateActiveCardConsciousness,
|
||||
updateActiveCardDisplayModel,
|
||||
persistActiveCardModuleSelections,
|
||||
updateActiveCardSpeech,
|
||||
updateActiveCardVision,
|
||||
getCard,
|
||||
resetState,
|
||||
initialize,
|
||||
activateCard,
|
||||
configureForAuthentication,
|
||||
clearProviderSelections,
|
||||
|
||||
currentModels: computed(() => {
|
||||
const {
|
||||
@@ -508,8 +615,9 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
'activateCard',
|
||||
'addCard',
|
||||
'initialize',
|
||||
'configureForAuthentication',
|
||||
'clearProviderSelections',
|
||||
'removeCard',
|
||||
'persistActiveCardModuleSelections',
|
||||
'updateActiveCardConsciousness',
|
||||
'updateActiveCardDisplayModel',
|
||||
'updateActiveCardSpeech',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OFFICIAL_CHAT_PROVIDER_ID, OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_TRANSCRIPTION_PROVIDER_ID, OFFICIAL_VISION_PROVIDER_ID } from '../../libs/providers/providers/official'
|
||||
import { OFFICIAL_CHAT_PROVIDER_ID, OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_TRANSCRIPTION_PROVIDER_ID, OFFICIAL_VISION_PROVIDER_ID } from '../../libs/providers/providers/official/constants'
|
||||
import { useProviderConfigStore } from '../providers/config'
|
||||
import { useProviderStore } from '../providers/provider'
|
||||
import { useConsciousnessStore } from './consciousness'
|
||||
|
||||
Reference in New Issue
Block a user