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 { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
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 { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||||
@@ -57,8 +56,7 @@ async function removeAuthenticationProviderConfiguration() {
|
|||||||
if (!syncedPinia.isLeader())
|
if (!syncedPinia.isLeader())
|
||||||
return
|
return
|
||||||
|
|
||||||
if (await unconfigureAuthenticationProviders())
|
await cardStore.configureForAuthentication(false)
|
||||||
await cardStore.persistActiveCardModuleSelections()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAuthenticatedSetup() {
|
function registerAuthenticatedSetup() {
|
||||||
@@ -66,8 +64,7 @@ function registerAuthenticatedSetup() {
|
|||||||
if (!syncedPinia.isLeader())
|
if (!syncedPinia.isLeader())
|
||||||
return
|
return
|
||||||
|
|
||||||
if (await configureAsDefaultsIfEmpty())
|
await cardStore.configureForAuthentication(true)
|
||||||
await cardStore.persistActiveCardModuleSelections()
|
|
||||||
await onboardingStore.closeAfterAuthentication()
|
await onboardingStore.closeAfterAuthentication()
|
||||||
})
|
})
|
||||||
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
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 { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
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 { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||||
@@ -142,8 +141,7 @@ function createFullStageRuntime() {
|
|||||||
if (!syncedPinia.isLeader())
|
if (!syncedPinia.isLeader())
|
||||||
return
|
return
|
||||||
|
|
||||||
if (await unconfigureAuthenticationProviders())
|
await cardStore.configureForAuthentication(false)
|
||||||
await cardStore.persistActiveCardModuleSelections()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAuthenticatedSetup() {
|
function registerAuthenticatedSetup() {
|
||||||
@@ -151,8 +149,7 @@ function createFullStageRuntime() {
|
|||||||
if (!syncedPinia.isLeader())
|
if (!syncedPinia.isLeader())
|
||||||
return
|
return
|
||||||
|
|
||||||
if (await configureAsDefaultsIfEmpty())
|
await cardStore.configureForAuthentication(true)
|
||||||
await cardStore.persistActiveCardModuleSelections()
|
|
||||||
await onboardingStore.closeAfterAuthentication()
|
await onboardingStore.closeAfterAuthentication()
|
||||||
})
|
})
|
||||||
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
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 { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
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 { useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
|
||||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||||
@@ -62,8 +61,7 @@ async function removeAuthenticationProviderConfiguration() {
|
|||||||
if (!syncedPinia.isLeader())
|
if (!syncedPinia.isLeader())
|
||||||
return
|
return
|
||||||
|
|
||||||
if (await unconfigureAuthenticationProviders())
|
await cardStore.configureForAuthentication(false)
|
||||||
await cardStore.persistActiveCardModuleSelections()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAuthenticatedSetup() {
|
function registerAuthenticatedSetup() {
|
||||||
@@ -71,8 +69,7 @@ function registerAuthenticatedSetup() {
|
|||||||
if (!syncedPinia.isLeader())
|
if (!syncedPinia.isLeader())
|
||||||
return
|
return
|
||||||
|
|
||||||
if (await configureAsDefaultsIfEmpty())
|
await cardStore.configureForAuthentication(true)
|
||||||
await cardStore.persistActiveCardModuleSelections()
|
|
||||||
await onboardingStore.closeAfterAuthentication()
|
await onboardingStore.closeAfterAuthentication()
|
||||||
})
|
})
|
||||||
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
stopLoggedOutSetup ??= authStore.onLogout(removeAuthenticationProviderConfiguration)
|
||||||
|
|||||||
@@ -414,10 +414,10 @@ pages:
|
|||||||
systemprompt: You will receive messages, answer to them like a real human.
|
systemprompt: You will receive messages, answer to them like a real human.
|
||||||
posthistoryinstructions: Remember to imitate an human.
|
posthistoryinstructions: Remember to imitate an human.
|
||||||
modules_info: >-
|
modules_info: >-
|
||||||
Configure modules for this character card. If not set, default module
|
Configure modules for this character card. Empty fields inherit global settings.
|
||||||
configuration will be used.
|
|
||||||
use_default: Use default
|
use_default: Use default
|
||||||
use_default_not_configured: Use default (not configured)
|
use_default_not_configured: Use default (not configured)
|
||||||
|
inherit_global_settings: Inherit global settings
|
||||||
fields_info:
|
fields_info:
|
||||||
subtitle: >-
|
subtitle: >-
|
||||||
You can put here some details about the character you are creating,
|
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.
|
consciousness_model: Select the consciousness model (AI model) for this character.
|
||||||
speech_model: Select the speech synthesis model and voice for this character.
|
speech_model: Select the speech synthesis model and voice for this character.
|
||||||
errors:
|
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.
|
name: Name should be valid or non-empty.
|
||||||
version: 'Error: Invalid version number !'
|
version: 'Error: Invalid version number !'
|
||||||
description: 'Error: You must provide a description for this card.'
|
description: 'Error: You must provide a description for this card.'
|
||||||
|
|||||||
@@ -399,9 +399,10 @@ pages:
|
|||||||
systemprompt: 你将收到消息,请像真实人类一样回复。
|
systemprompt: 你将收到消息,请像真实人类一样回复。
|
||||||
posthistoryinstructions: 记得模仿人类的行为。
|
posthistoryinstructions: 记得模仿人类的行为。
|
||||||
modules_info: >-
|
modules_info: >-
|
||||||
为此角色卡配置模块。如未设置,将使用默认模块配置。
|
配置此角色卡的模块。空字段将继承全局设置。
|
||||||
use_default: 使用默认
|
use_default: 使用默认
|
||||||
use_default_not_configured: 使用默认(未配置)
|
use_default_not_configured: 使用默认(未配置)
|
||||||
|
inherit_global_settings: 继承全局设置
|
||||||
fields_info:
|
fields_info:
|
||||||
subtitle: >-
|
subtitle: >-
|
||||||
您可以在这里填写有关您正在创建的角色的一些详细信息,解释他的背景和情境,以及应该如何回应您的互动。
|
您可以在这里填写有关您正在创建的角色的一些详细信息,解释他的背景和情境,以及应该如何回应您的互动。
|
||||||
@@ -421,6 +422,7 @@ pages:
|
|||||||
consciousness_model: 为这个角色选择 AI 模型。
|
consciousness_model: 为这个角色选择 AI 模型。
|
||||||
speech_model: 为此角色指定语音合成模型与音色。
|
speech_model: 为此角色指定语音合成模型与音色。
|
||||||
errors:
|
errors:
|
||||||
|
model_required: 请为指定的提供商选择模型,不能继承其他提供商的模型。
|
||||||
name: 错误:你必须提供一个有效的名称!
|
name: 错误:你必须提供一个有效的名称!
|
||||||
version: '错误:版本号无效!'
|
version: '错误:版本号无效!'
|
||||||
description: '错误:你必须为此卡片提供描述。'
|
description: '错误:你必须为此卡片提供描述。'
|
||||||
|
|||||||
+157
-107
@@ -1,19 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Card } from '@proj-airi/ccc'
|
import type { Card } from '@proj-airi/ccc'
|
||||||
import type { AiriExtension } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
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 { isCustomProvidersDisabled } from '@proj-airi/stage-shared'
|
||||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||||
import { DEFAULT_ARTISTRY_WIDGET_INSTRUCTION } from '@proj-airi/stage-ui/constants/prompts/artistry-instruction'
|
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 { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
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 { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||||
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
|
||||||
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
import { useVisionStore } from '@proj-airi/stage-ui/stores/modules/vision'
|
||||||
import { useProviderStore } from '@proj-airi/stage-ui/stores/providers/provider'
|
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 { Button, FieldInput, FieldValues } from '@proj-airi/ui'
|
||||||
import { ComboboxSelect } from '@proj-airi/ui/components/form'
|
import { ComboboxSelect } from '@proj-airi/ui/components/form'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
@@ -65,15 +65,11 @@ const visionStore = useVisionStore()
|
|||||||
const speechStore = useSpeechStore()
|
const speechStore = useSpeechStore()
|
||||||
const providersStore = useProviderStore()
|
const providersStore = useProviderStore()
|
||||||
const displayModelsStore = useDisplayModelsStore()
|
const displayModelsStore = useDisplayModelsStore()
|
||||||
const stageModelStore = useSettingsStageModel()
|
|
||||||
const artistryStore = useArtistryStore()
|
|
||||||
|
|
||||||
const { activeProvider: consciousnessProvider, activeModel: defaultConsciousnessModel } = storeToRefs(consciousnessStore)
|
const consciousnessProvider = computed(() => cardStore.moduleDefaults?.consciousness.provider ?? '')
|
||||||
const { activeProvider: visionProvider, activeModel: defaultVisionModel } = storeToRefs(visionStore)
|
const visionProvider = computed(() => cardStore.moduleDefaults?.vision.provider ?? '')
|
||||||
const { activeSpeechProvider: speechProvider, activeSpeechModel: defaultSpeechModel, activeSpeechVoiceId: defaultSpeechVoiceId } = storeToRefs(speechStore)
|
const speechProvider = computed(() => cardStore.moduleDefaults?.speech.provider ?? '')
|
||||||
const { displayModels } = storeToRefs(displayModelsStore)
|
const { displayModels } = storeToRefs(displayModelsStore)
|
||||||
const { stageModelSelected: defaultDisplayModelId } = storeToRefs(stageModelStore)
|
|
||||||
const { activeProvider: defaultArtistryProvider } = storeToRefs(artistryStore)
|
|
||||||
|
|
||||||
// Determine if we're in edit mode
|
// Determine if we're in edit mode
|
||||||
const isEditMode = computed(() => !!props.cardId)
|
const isEditMode = computed(() => !!props.cardId)
|
||||||
@@ -89,8 +85,34 @@ const selectedSpeechModel = ref<string>('')
|
|||||||
const selectedSpeechVoiceId = ref<string>('')
|
const selectedSpeechVoiceId = ref<string>('')
|
||||||
const selectedDisplayModelId = 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
|
// Artistry configuration
|
||||||
const selectedArtistryProvider = ref<string>('')
|
const selectedArtistryProvider = ref<string>('')
|
||||||
|
const artistryProviderSelection = createInheritableSelection(selectedArtistryProvider)
|
||||||
const selectedArtistryModel = ref<string>('')
|
const selectedArtistryModel = ref<string>('')
|
||||||
const selectedArtistryPromptPrefix = ref<string>('')
|
const selectedArtistryPromptPrefix = ref<string>('')
|
||||||
const selectedArtistryWidgetInstruction = ref<string>('')
|
const selectedArtistryWidgetInstruction = ref<string>('')
|
||||||
@@ -101,89 +123,106 @@ const selectedArtistryConfigStr = ref<string>('{\n \n}')
|
|||||||
let isInitializingModuleSelections = false
|
let isInitializingModuleSelections = false
|
||||||
let hasLoadedModuleOptions = 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
|
// Computed: available display model options
|
||||||
const displayModelOptions = computed(() =>
|
const displayModelOptions = computed(() =>
|
||||||
displayModels.value.map(model => ({
|
withInheritGlobalSetting(displayModels.value.map(model => ({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
label: model.name,
|
label: model.name,
|
||||||
})),
|
})), selectedDisplayModelId.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Computed: available consciousness provider options
|
// Computed: available consciousness provider options
|
||||||
const consciousnessProviderOptions = computed(() => {
|
const consciousnessProviderOptions = computed(() => {
|
||||||
return providersStore.configuredChatProvidersMetadata.map(provider => ({
|
return withInheritGlobalSetting(providersStore.configuredChatProvidersMetadata.map(provider => ({
|
||||||
value: provider.id,
|
value: provider.id,
|
||||||
label: provider.localizedName || provider.name,
|
label: provider.localizedName || provider.name,
|
||||||
}))
|
})), selectedConsciousnessProvider.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed: available consciousness models options
|
// Computed: available consciousness models options
|
||||||
const consciousnessModelOptions = computed(() => {
|
const consciousnessModelOptions = computed(() => {
|
||||||
const provider = selectedConsciousnessProvider.value || consciousnessProvider.value
|
const provider = selectedConsciousnessProvider.value || consciousnessProvider.value
|
||||||
if (!provider)
|
if (!provider)
|
||||||
return []
|
return withInheritGlobalSetting([], selectedConsciousnessModel.value)
|
||||||
const models = providersStore.getModelsForProvider(provider)
|
const models = providersStore.getModelsForProvider(provider)
|
||||||
return models.map(model => ({
|
return withInheritGlobalSetting(models.map(model => ({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
label: model.name || model.id,
|
label: model.name || model.id,
|
||||||
}))
|
})), selectedConsciousnessModel.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed: available vision provider options
|
// Computed: available vision provider options
|
||||||
const visionProviderOptions = computed(() => {
|
const visionProviderOptions = computed(() => {
|
||||||
return providersStore.configuredVisionProvidersMetadata.map(provider => ({
|
return withInheritGlobalSetting(providersStore.configuredVisionProvidersMetadata.map(provider => ({
|
||||||
value: provider.id,
|
value: provider.id,
|
||||||
label: provider.localizedName || provider.name,
|
label: provider.localizedName || provider.name,
|
||||||
}))
|
})), selectedVisionProvider.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed: available vision models options
|
// Computed: available vision models options
|
||||||
const visionModelOptions = computed(() => {
|
const visionModelOptions = computed(() => {
|
||||||
const provider = selectedVisionProvider.value || visionProvider.value
|
const provider = selectedVisionProvider.value || visionProvider.value
|
||||||
if (!provider)
|
if (!provider)
|
||||||
return []
|
return withInheritGlobalSetting([], selectedVisionModel.value)
|
||||||
const models = providersStore.getModelsForProvider(provider)
|
const models = providersStore.getModelsForProvider(provider)
|
||||||
return models.map(model => ({
|
return withInheritGlobalSetting(models.map(model => ({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
label: model.name || model.id,
|
label: model.name || model.id,
|
||||||
}))
|
})), selectedVisionModel.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed: available speech provider options
|
// Computed: available speech provider options
|
||||||
const speechProviderOptions = computed(() => {
|
const speechProviderOptions = computed(() => {
|
||||||
return providersStore.configuredSpeechProvidersMetadata.map(provider => ({
|
return withInheritGlobalSetting(providersStore.configuredSpeechProvidersMetadata.map(provider => ({
|
||||||
value: provider.id,
|
value: provider.id,
|
||||||
label: provider.localizedName || provider.name,
|
label: provider.localizedName || provider.name,
|
||||||
}))
|
})), selectedSpeechProvider.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed: available speech models options
|
// Computed: available speech models options
|
||||||
const speechModelOptions = computed(() => {
|
const speechModelOptions = computed(() => {
|
||||||
const provider = selectedSpeechProvider.value || speechProvider.value
|
const provider = selectedSpeechProvider.value || speechProvider.value
|
||||||
if (!provider)
|
if (!provider)
|
||||||
return []
|
return withInheritGlobalSetting([], selectedSpeechModel.value)
|
||||||
const models = providersStore.getModelsForProvider(provider)
|
const models = providersStore.getModelsForProvider(provider)
|
||||||
return models.map(model => ({
|
return withInheritGlobalSetting(models.map(model => ({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
label: model.name || model.id,
|
label: model.name || model.id,
|
||||||
}))
|
})), selectedSpeechModel.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed: available speech voices options
|
// Computed: available speech voices options
|
||||||
const speechVoiceOptions = computed(() => {
|
const speechVoiceOptions = computed(() => {
|
||||||
const provider = selectedSpeechProvider.value || speechProvider.value
|
const provider = selectedSpeechProvider.value || speechProvider.value
|
||||||
if (!provider)
|
if (!provider)
|
||||||
return []
|
return withInheritGlobalSetting([], selectedSpeechVoiceId.value)
|
||||||
const voices = speechStore.getVoicesForProvider(provider)
|
const voices = speechStore.getVoicesForProvider(provider)
|
||||||
return voices.map(voice => ({
|
return withInheritGlobalSetting(voices.map(voice => ({
|
||||||
value: voice.id,
|
value: voice.id,
|
||||||
label: voice.name || voice.id,
|
label: voice.name || voice.id,
|
||||||
}))
|
})), selectedSpeechVoiceId.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed: available artistry provider options
|
// Computed: available artistry provider options
|
||||||
const artistryProviderOptions = computed(() => {
|
const artistryProviderOptions = computed(() => {
|
||||||
return [
|
return withInheritGlobalSetting([
|
||||||
{ value: 'none', label: 'None (Disabled)' },
|
{ value: 'none', label: 'None (Disabled)' },
|
||||||
{ value: 'comfyui', label: 'ComfyUI' },
|
{ value: 'comfyui', label: 'ComfyUI' },
|
||||||
...(isCustomProvidersDisabled()
|
...(isCustomProvidersDisabled()
|
||||||
@@ -192,7 +231,7 @@ const artistryProviderOptions = computed(() => {
|
|||||||
{ value: 'replicate', label: 'Replicate' },
|
{ value: 'replicate', label: 'Replicate' },
|
||||||
{ value: 'nanobanana', label: 'Nano Banana' },
|
{ value: 'nanobanana', label: 'Nano Banana' },
|
||||||
]),
|
]),
|
||||||
]
|
], selectedArtistryProvider.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadSelectedModuleOptions() {
|
async function loadSelectedModuleOptions() {
|
||||||
@@ -201,16 +240,19 @@ async function loadSelectedModuleOptions() {
|
|||||||
|
|
||||||
hasLoadedModuleOptions = true
|
hasLoadedModuleOptions = true
|
||||||
const loads: Promise<unknown>[] = []
|
const loads: Promise<unknown>[] = []
|
||||||
if (selectedConsciousnessProvider.value)
|
const consciousnessProviderId = selectedConsciousnessProvider.value || consciousnessProvider.value
|
||||||
loads.push(consciousnessStore.loadModelsForProvider(selectedConsciousnessProvider.value))
|
if (consciousnessProviderId)
|
||||||
|
loads.push(consciousnessStore.loadModelsForProvider(consciousnessProviderId))
|
||||||
|
|
||||||
if (selectedVisionProvider.value)
|
const visionProviderId = selectedVisionProvider.value || visionProvider.value
|
||||||
loads.push(visionStore.loadModelsForProvider(selectedVisionProvider.value))
|
if (visionProviderId)
|
||||||
|
loads.push(visionStore.loadModelsForProvider(visionProviderId))
|
||||||
|
|
||||||
if (selectedSpeechProvider.value) {
|
const speechProviderId = selectedSpeechProvider.value || speechProvider.value
|
||||||
loads.push(speechStore.loadVoicesForProvider(selectedSpeechProvider.value, selectedSpeechModel.value || undefined))
|
if (speechProviderId) {
|
||||||
if (providersStore.supportsModelListing(selectedSpeechProvider.value))
|
loads.push(speechStore.loadVoicesForProvider(speechProviderId, selectedSpeechModel.value || undefined))
|
||||||
loads.push(providersStore.fetchModelsForProvider(selectedSpeechProvider.value))
|
if (providersStore.supportsModelListing(speechProviderId))
|
||||||
|
loads.push(providersStore.fetchModelsForProvider(speechProviderId))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 consciousness provider changes and reload models
|
||||||
watch(selectedConsciousnessProvider, async (newProvider, oldProvider) => {
|
watch(selectedConsciousnessProvider, async (newProvider, oldProvider) => {
|
||||||
if (props.modelValue && !isInitializingModuleSelections && oldProvider !== undefined && newProvider !== oldProvider && newProvider) {
|
if (props.modelValue && !isInitializingModuleSelections && newProvider !== oldProvider) {
|
||||||
await consciousnessStore.loadModelsForProvider(newProvider)
|
|
||||||
// Reset model selection to default or empty
|
|
||||||
selectedConsciousnessModel.value = ''
|
selectedConsciousnessModel.value = ''
|
||||||
|
await consciousnessStore.loadModelsForProvider(newProvider || consciousnessProvider.value)
|
||||||
}
|
}
|
||||||
})
|
}, { flush: 'sync' })
|
||||||
|
|
||||||
// Watch vision provider changes and reload models
|
// Watch vision provider changes and reload models
|
||||||
watch(selectedVisionProvider, async (newProvider, oldProvider) => {
|
watch(selectedVisionProvider, async (newProvider, oldProvider) => {
|
||||||
if (props.modelValue && !isInitializingModuleSelections && oldProvider !== undefined && newProvider !== oldProvider && newProvider) {
|
if (props.modelValue && !isInitializingModuleSelections && newProvider !== oldProvider) {
|
||||||
await visionStore.loadModelsForProvider(newProvider)
|
|
||||||
selectedVisionModel.value = ''
|
selectedVisionModel.value = ''
|
||||||
|
await visionStore.loadModelsForProvider(newProvider || visionProvider.value)
|
||||||
}
|
}
|
||||||
})
|
}, { flush: 'sync' })
|
||||||
|
|
||||||
// Watch speech provider changes and reload models/voices
|
// Watch speech provider changes and reload models/voices
|
||||||
watch(selectedSpeechProvider, async (newProvider, oldProvider) => {
|
watch(selectedSpeechProvider, async (newProvider, oldProvider) => {
|
||||||
if (props.modelValue && !isInitializingModuleSelections && oldProvider !== undefined && newProvider !== oldProvider && newProvider) {
|
if (props.modelValue && !isInitializingModuleSelections && newProvider !== oldProvider) {
|
||||||
await speechStore.loadVoicesForProvider(newProvider)
|
|
||||||
if (providersStore.supportsModelListing(newProvider)) {
|
|
||||||
await providersStore.fetchModelsForProvider(newProvider)
|
|
||||||
}
|
|
||||||
// Reset model and voice selection
|
|
||||||
selectedSpeechModel.value = ''
|
selectedSpeechModel.value = ''
|
||||||
selectedSpeechVoiceId.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)
|
// Reset voice when speech model changes (different models may have different voices)
|
||||||
watch(selectedSpeechModel, async (newModel, oldModel) => {
|
watch(selectedSpeechModel, async (newModel, oldModel) => {
|
||||||
// Only reset if model actually changed and we're not initializing
|
// Only reset if model actually changed and we're not initializing
|
||||||
const provider = selectedSpeechProvider.value || speechProvider.value
|
const provider = selectedSpeechProvider.value || speechProvider.value
|
||||||
if (props.modelValue && !isInitializingModuleSelections && oldModel !== undefined && newModel !== oldModel && provider) {
|
if (props.modelValue && !isInitializingModuleSelections && oldModel !== undefined && newModel !== oldModel && provider) {
|
||||||
// Reload voices for the current provider
|
selectedSpeechVoiceId.value = ''
|
||||||
await speechStore.loadVoicesForProvider(provider)
|
await speechStore.loadVoicesForProvider(provider, newModel || undefined)
|
||||||
|
|
||||||
// Reset voice selection to default
|
|
||||||
selectedSpeechVoiceId.value = defaultSpeechVoiceId.value || ''
|
|
||||||
}
|
}
|
||||||
})
|
}, { flush: 'sync' })
|
||||||
|
|
||||||
// Tab type definition
|
// Tab type definition
|
||||||
interface Tab {
|
interface Tab {
|
||||||
@@ -322,6 +364,20 @@ const showError = ref<boolean>(false)
|
|||||||
const errorMessage = ref<string>('')
|
const errorMessage = ref<string>('')
|
||||||
|
|
||||||
async function saveCard(card: Card, activate: boolean): Promise<boolean> {
|
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)
|
const draftResult = safeParseAiriCardDraft(toRaw(card), selectedArtistryConfigStr.value)
|
||||||
if (!draftResult.success) {
|
if (!draftResult.success) {
|
||||||
showError.value = true
|
showError.value = true
|
||||||
@@ -334,21 +390,21 @@ async function saveCard(card: Card, activate: boolean): Promise<boolean> {
|
|||||||
|
|
||||||
const cardWithModules = applyAiriCardEditorModules(rawCard, {
|
const cardWithModules = applyAiriCardEditorModules(rawCard, {
|
||||||
consciousness: {
|
consciousness: {
|
||||||
provider: selectedConsciousnessProvider.value || consciousnessProvider.value,
|
provider: selectedConsciousnessProvider.value,
|
||||||
model: selectedConsciousnessModel.value || defaultConsciousnessModel.value,
|
model: selectedConsciousnessModel.value,
|
||||||
},
|
},
|
||||||
vision: {
|
vision: {
|
||||||
provider: selectedVisionProvider.value || visionProvider.value,
|
provider: selectedVisionProvider.value,
|
||||||
model: selectedVisionModel.value || defaultVisionModel.value,
|
model: selectedVisionModel.value,
|
||||||
},
|
},
|
||||||
speech: {
|
speech: {
|
||||||
provider: selectedSpeechProvider.value || speechProvider.value,
|
provider: selectedSpeechProvider.value,
|
||||||
model: selectedSpeechModel.value || defaultSpeechModel.value,
|
model: selectedSpeechModel.value,
|
||||||
voice_id: selectedSpeechVoiceId.value || defaultSpeechVoiceId.value,
|
voice_id: selectedSpeechVoiceId.value,
|
||||||
},
|
},
|
||||||
displayModelId: selectedDisplayModelId.value || defaultDisplayModelId.value,
|
displayModelId: selectedDisplayModelId.value,
|
||||||
artistry: {
|
artistry: {
|
||||||
provider: selectedArtistryProvider.value || defaultArtistryProvider.value,
|
provider: selectedArtistryProvider.value,
|
||||||
model: selectedArtistryModel.value,
|
model: selectedArtistryModel.value,
|
||||||
promptPrefix: selectedArtistryPromptPrefix.value,
|
promptPrefix: selectedArtistryPromptPrefix.value,
|
||||||
widgetInstruction: selectedArtistryWidgetInstruction.value,
|
widgetInstruction: selectedArtistryWidgetInstruction.value,
|
||||||
@@ -404,19 +460,19 @@ function initializeCard(): Card {
|
|||||||
const existingCard = (isEditMode.value && props.cardId) ? cardStore.getCard(props.cardId) : undefined
|
const existingCard = (isEditMode.value && props.cardId) ? cardStore.getCard(props.cardId) : undefined
|
||||||
const airiExt = existingCard?.extensions?.airi as AiriExtensionWithLegacyArtistry | undefined
|
const airiExt = existingCard?.extensions?.airi as AiriExtensionWithLegacyArtistry | undefined
|
||||||
|
|
||||||
// Initialize module selections with fallback logic (handles all cases: create, edit with/without extension)
|
const moduleSettings = getAiriCardEditorModuleSettings(existingCard)
|
||||||
selectedConsciousnessProvider.value = airiExt?.modules?.consciousness?.provider || consciousnessProvider.value
|
selectedConsciousnessProvider.value = moduleSettings.consciousness.provider
|
||||||
selectedConsciousnessModel.value = airiExt?.modules?.consciousness?.model || defaultConsciousnessModel.value
|
selectedConsciousnessModel.value = moduleSettings.consciousness.model
|
||||||
selectedVisionProvider.value = airiExt?.modules?.vision?.provider || visionProvider.value
|
selectedVisionProvider.value = moduleSettings.vision.provider
|
||||||
selectedVisionModel.value = airiExt?.modules?.vision?.model || defaultVisionModel.value
|
selectedVisionModel.value = moduleSettings.vision.model
|
||||||
selectedSpeechProvider.value = airiExt?.modules?.speech?.provider || speechProvider.value
|
selectedSpeechProvider.value = moduleSettings.speech.provider
|
||||||
selectedSpeechModel.value = airiExt?.modules?.speech?.model || defaultSpeechModel.value
|
selectedSpeechModel.value = moduleSettings.speech.model
|
||||||
selectedSpeechVoiceId.value = airiExt?.modules?.speech?.voice_id || defaultSpeechVoiceId.value
|
selectedSpeechVoiceId.value = moduleSettings.speech.voice_id
|
||||||
selectedDisplayModelId.value = airiExt?.modules?.displayModelId || defaultDisplayModelId.value
|
selectedDisplayModelId.value = moduleSettings.displayModelId ?? ''
|
||||||
|
|
||||||
// NOTICE: keep legacy `extensions.airi.artistry` fallback so existing cards continue to load.
|
// NOTICE: keep legacy `extensions.airi.artistry` fallback so existing cards continue to load.
|
||||||
const artistrySettings = airiExt?.modules?.artistry || airiExt?.artistry
|
const artistrySettings = airiExt?.modules?.artistry || airiExt?.artistry
|
||||||
selectedArtistryProvider.value = artistrySettings?.provider || defaultArtistryProvider.value
|
selectedArtistryProvider.value = artistrySettings?.provider ?? ''
|
||||||
selectedArtistryModel.value = artistrySettings?.model || ''
|
selectedArtistryModel.value = artistrySettings?.model || ''
|
||||||
selectedArtistryPromptPrefix.value = artistrySettings?.promptPrefix || ''
|
selectedArtistryPromptPrefix.value = artistrySettings?.promptPrefix || ''
|
||||||
selectedArtistryWidgetInstruction.value = artistrySettings?.widgetInstruction || DEFAULT_ARTISTRY_WIDGET_INSTRUCTION
|
selectedArtistryWidgetInstruction.value = artistrySettings?.widgetInstruction || DEFAULT_ARTISTRY_WIDGET_INSTRUCTION
|
||||||
@@ -425,10 +481,10 @@ function initializeCard(): Card {
|
|||||||
selectedArtistryAutonomousThreshold.value = (artistrySettings as any)?.autonomousThreshold ?? 70
|
selectedArtistryAutonomousThreshold.value = (artistrySettings as any)?.autonomousThreshold ?? 70
|
||||||
|
|
||||||
try {
|
try {
|
||||||
selectedArtistryConfigStr.value = artistrySettings?.options ? JSON.stringify(artistrySettings.options, null, 2) : '{\n \n}'
|
selectedArtistryConfigStr.value = artistrySettings?.options ? JSON.stringify(artistrySettings.options, null, 2) : ''
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
selectedArtistryConfigStr.value = '{\n \n}'
|
selectedArtistryConfigStr.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return existing card data or defaults
|
// Return existing card data or defaults
|
||||||
@@ -490,10 +546,8 @@ const cardSystemPrompt = makeComputed('systemPrompt')
|
|||||||
const cardPostHistoryInstructions = makeComputed('postHistoryInstructions')
|
const cardPostHistoryInstructions = makeComputed('postHistoryInstructions')
|
||||||
|
|
||||||
// Helper function to generate placeholder text for default values
|
// Helper function to generate placeholder text for default values
|
||||||
function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
function getDefaultPlaceholder(): string {
|
||||||
return defaultValue
|
return t('settings.pages.card.creation.inherit_global_settings')
|
||||||
? `${t('settings.pages.card.creation.use_default')} (${defaultValue})`
|
|
||||||
: t('settings.pages.card.creation.use_default_not_configured')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -574,9 +628,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
|||||||
{{ t('settings.pages.card.chat.provider') }}
|
{{ t('settings.pages.card.chat.provider') }}
|
||||||
</label>
|
</label>
|
||||||
<ComboboxSelect
|
<ComboboxSelect
|
||||||
v-model="selectedConsciousnessProvider"
|
v-model="consciousnessProviderSelection"
|
||||||
:options="consciousnessProviderOptions"
|
:options="consciousnessProviderOptions"
|
||||||
:placeholder="getDefaultPlaceholder(consciousnessProvider)"
|
:placeholder="getDefaultPlaceholder()"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -588,10 +642,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
|||||||
{{ t('settings.pages.card.consciousness.model') }}
|
{{ t('settings.pages.card.consciousness.model') }}
|
||||||
</label>
|
</label>
|
||||||
<ComboboxSelect
|
<ComboboxSelect
|
||||||
v-model="selectedConsciousnessModel"
|
v-model="consciousnessModelSelection"
|
||||||
:options="consciousnessModelOptions"
|
:options="consciousnessModelOptions"
|
||||||
:placeholder="getDefaultPlaceholder(defaultConsciousnessModel)"
|
:placeholder="getDefaultPlaceholder()"
|
||||||
:disabled="!selectedConsciousnessProvider && !consciousnessProvider"
|
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -603,9 +656,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
|||||||
{{ t('settings.pages.card.vision.provider') }}
|
{{ t('settings.pages.card.vision.provider') }}
|
||||||
</label>
|
</label>
|
||||||
<ComboboxSelect
|
<ComboboxSelect
|
||||||
v-model="selectedVisionProvider"
|
v-model="visionProviderSelection"
|
||||||
:options="visionProviderOptions"
|
:options="visionProviderOptions"
|
||||||
:placeholder="getDefaultPlaceholder(visionProvider)"
|
:placeholder="getDefaultPlaceholder()"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -617,10 +670,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
|||||||
{{ t('settings.pages.card.vision.model') }}
|
{{ t('settings.pages.card.vision.model') }}
|
||||||
</label>
|
</label>
|
||||||
<ComboboxSelect
|
<ComboboxSelect
|
||||||
v-model="selectedVisionModel"
|
v-model="visionModelSelection"
|
||||||
:options="visionModelOptions"
|
:options="visionModelOptions"
|
||||||
:placeholder="getDefaultPlaceholder(defaultVisionModel)"
|
:placeholder="getDefaultPlaceholder()"
|
||||||
:disabled="!selectedVisionProvider && !visionProvider"
|
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -632,9 +684,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
|||||||
{{ t('settings.pages.card.speech.provider') }}
|
{{ t('settings.pages.card.speech.provider') }}
|
||||||
</label>
|
</label>
|
||||||
<ComboboxSelect
|
<ComboboxSelect
|
||||||
v-model="selectedSpeechProvider"
|
v-model="speechProviderSelection"
|
||||||
:options="speechProviderOptions"
|
:options="speechProviderOptions"
|
||||||
:placeholder="getDefaultPlaceholder(speechProvider)"
|
:placeholder="getDefaultPlaceholder()"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -646,10 +698,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
|||||||
{{ t('settings.pages.card.speech.model') }}
|
{{ t('settings.pages.card.speech.model') }}
|
||||||
</label>
|
</label>
|
||||||
<ComboboxSelect
|
<ComboboxSelect
|
||||||
v-model="selectedSpeechModel"
|
v-model="speechModelSelection"
|
||||||
:options="speechModelOptions"
|
:options="speechModelOptions"
|
||||||
:placeholder="getDefaultPlaceholder(defaultSpeechModel)"
|
:placeholder="getDefaultPlaceholder()"
|
||||||
:disabled="!selectedSpeechProvider && !speechProvider"
|
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -661,10 +712,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
|||||||
{{ t('settings.pages.card.speech.voice') }}
|
{{ t('settings.pages.card.speech.voice') }}
|
||||||
</label>
|
</label>
|
||||||
<ComboboxSelect
|
<ComboboxSelect
|
||||||
v-model="selectedSpeechVoiceId"
|
v-model="speechVoiceSelection"
|
||||||
:options="speechVoiceOptions"
|
:options="speechVoiceOptions"
|
||||||
:placeholder="getDefaultPlaceholder(defaultSpeechVoiceId)"
|
:placeholder="getDefaultPlaceholder()"
|
||||||
:disabled="!selectedSpeechProvider && !speechProvider"
|
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -676,9 +726,9 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
|||||||
{{ t('settings.pages.card.body-model') }}
|
{{ t('settings.pages.card.body-model') }}
|
||||||
</label>
|
</label>
|
||||||
<ComboboxSelect
|
<ComboboxSelect
|
||||||
v-model="selectedDisplayModelId"
|
v-model="displayModelSelection"
|
||||||
:options="displayModelOptions"
|
:options="displayModelOptions"
|
||||||
:placeholder="getDefaultPlaceholder(defaultDisplayModelId)"
|
:placeholder="getDefaultPlaceholder()"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -695,7 +745,7 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
|||||||
<!-- Artistry -->
|
<!-- Artistry -->
|
||||||
<CardCreationTabArtistry
|
<CardCreationTabArtistry
|
||||||
v-else-if="activeTab === 'artistry'"
|
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-model="selectedArtistryModel"
|
||||||
v-model:selected-artistry-prompt-prefix="selectedArtistryPromptPrefix"
|
v-model:selected-artistry-prompt-prefix="selectedArtistryPromptPrefix"
|
||||||
v-model:selected-artistry-widget-instruction="selectedArtistryWidgetInstruction"
|
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-spawn-mode="selectedArtistrySpawnMode"
|
||||||
v-model:selected-artistry-config-str="selectedArtistryConfigStr"
|
v-model:selected-artistry-config-str="selectedArtistryConfigStr"
|
||||||
:artistry-provider-options="artistryProviderOptions"
|
: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">
|
<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 { useBackgroundStore } from '@proj-airi/stage-ui/stores/background'
|
||||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
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 { Button, Select } from '@proj-airi/ui'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import {
|
import {
|
||||||
@@ -41,17 +38,11 @@ const emit = defineEmits<{
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { trackSceneBackgroundSet } = useAnalytics()
|
const { trackSceneBackgroundSet } = useAnalytics()
|
||||||
const cardStore = useAiriCardStore()
|
const cardStore = useAiriCardStore()
|
||||||
const consciousnessStore = useConsciousnessStore()
|
|
||||||
const speechStore = useSpeechStore()
|
|
||||||
const visionStore = useVisionStore()
|
|
||||||
const backgroundStore = useBackgroundStore()
|
const backgroundStore = useBackgroundStore()
|
||||||
const displayModelsStore = useDisplayModelsStore()
|
const displayModelsStore = useDisplayModelsStore()
|
||||||
|
|
||||||
const { removeCard } = cardStore
|
const { removeCard } = cardStore
|
||||||
const { activeCardId } = storeToRefs(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 isRefreshingGallery = ref(false)
|
||||||
const isExportingCard = shallowRef(false)
|
const isExportingCard = shallowRef(false)
|
||||||
@@ -317,15 +308,13 @@ watch(() => props.modelValue, (isOpen) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Helper function to generate placeholder text for default values
|
// Helper function to generate placeholder text for default values
|
||||||
function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
function getDefaultPlaceholder(): string {
|
||||||
return defaultValue
|
return t('settings.pages.card.creation.inherit_global_settings')
|
||||||
? `${t('settings.pages.card.creation.use_default')} (${defaultValue})`
|
|
||||||
: t('settings.pages.card.creation.use_default_not_configured')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get display value for module settings
|
// Helper function to get display value for module settings
|
||||||
function getModuleDisplayValue(value: string | undefined, defaultValue: string | undefined): string {
|
function getModuleDisplayValue(value: string | undefined): string {
|
||||||
return value || getDefaultPlaceholder(defaultValue)
|
return value || getDefaultPlaceholder()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -466,7 +455,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
|||||||
{{ t('settings.pages.card.chat.provider') }}
|
{{ t('settings.pages.card.chat.provider') }}
|
||||||
</span>
|
</span>
|
||||||
<div truncate font-medium>
|
<div truncate font-medium>
|
||||||
{{ getModuleDisplayValue(moduleSettings.consciousnessProvider, consciousnessProvider) }}
|
{{ getModuleDisplayValue(moduleSettings.consciousnessProvider) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -483,7 +472,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
|||||||
{{ t('settings.pages.card.consciousness.model') }}
|
{{ t('settings.pages.card.consciousness.model') }}
|
||||||
</span>
|
</span>
|
||||||
<div truncate font-medium>
|
<div truncate font-medium>
|
||||||
{{ getModuleDisplayValue(moduleSettings.consciousness, defaultConsciousnessModel) }}
|
{{ getModuleDisplayValue(moduleSettings.consciousness) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -500,7 +489,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
|||||||
{{ t('settings.pages.card.vision.provider') }}
|
{{ t('settings.pages.card.vision.provider') }}
|
||||||
</span>
|
</span>
|
||||||
<div truncate font-medium>
|
<div truncate font-medium>
|
||||||
{{ getModuleDisplayValue(moduleSettings.visionProvider, visionProvider) }}
|
{{ getModuleDisplayValue(moduleSettings.visionProvider) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -517,7 +506,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
|||||||
{{ t('settings.pages.card.vision.model') }}
|
{{ t('settings.pages.card.vision.model') }}
|
||||||
</span>
|
</span>
|
||||||
<div truncate font-medium>
|
<div truncate font-medium>
|
||||||
{{ getModuleDisplayValue(moduleSettings.vision, defaultVisionModel) }}
|
{{ getModuleDisplayValue(moduleSettings.vision) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -534,7 +523,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
|||||||
{{ t('settings.pages.card.speech.provider') }}
|
{{ t('settings.pages.card.speech.provider') }}
|
||||||
</span>
|
</span>
|
||||||
<div truncate font-medium>
|
<div truncate font-medium>
|
||||||
{{ getModuleDisplayValue(moduleSettings.speechProvider, speechProvider) }}
|
{{ getModuleDisplayValue(moduleSettings.speechProvider) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -551,7 +540,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
|||||||
{{ t('settings.pages.card.speech.model') }}
|
{{ t('settings.pages.card.speech.model') }}
|
||||||
</span>
|
</span>
|
||||||
<div truncate font-medium>
|
<div truncate font-medium>
|
||||||
{{ getModuleDisplayValue(moduleSettings.speech, defaultSpeechModel) }}
|
{{ getModuleDisplayValue(moduleSettings.speech) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -568,7 +557,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
|||||||
{{ t('settings.pages.card.speech.voice') }}
|
{{ t('settings.pages.card.speech.voice') }}
|
||||||
</span>
|
</span>
|
||||||
<div truncate font-medium>
|
<div truncate font-medium>
|
||||||
{{ getModuleDisplayValue(moduleSettings.voice, defaultVoiceId) }}
|
{{ getModuleDisplayValue(moduleSettings.voice) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,28 +42,26 @@ watch(activeProvider, async (provider) => {
|
|||||||
await consciousnessStore.loadModelsForProvider(provider)
|
await consciousnessStore.loadModelsForProvider(provider)
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
watch([activeProvider, activeModel], ([provider, model]) => {
|
async function persistSelection() {
|
||||||
void airiCardStore.updateActiveCardConsciousness({ provider, model })
|
await airiCardStore.updateActiveCardConsciousness({ provider: activeProvider.value, model: activeModel.value })
|
||||||
})
|
}
|
||||||
|
|
||||||
function updateCustomModelName(value: string) {
|
function updateCustomModelName(value: string) {
|
||||||
customModelName.value = value
|
customModelName.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectModel(modelId: string) {
|
async function selectModel(modelId: string) {
|
||||||
const previousModelId = activeModel.value
|
const previousModelId = activeModel.value
|
||||||
activeModel.value = modelId
|
activeModel.value = modelId
|
||||||
|
await persistSelection()
|
||||||
|
|
||||||
if (previousModelId !== modelId)
|
if (previousModelId !== modelId)
|
||||||
trackModelSwitched(previousModelId || 'none', modelId)
|
trackModelSwitched(previousModelId || 'none', modelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteProvider(providerId: string) {
|
async function handleDeleteProvider(providerId: string) {
|
||||||
if (activeProvider.value === providerId) {
|
await airiCardStore.clearProviderSelections(providerId)
|
||||||
activeProvider.value = ''
|
await providersStore.deleteProvider(providerId)
|
||||||
activeModel.value = ''
|
|
||||||
}
|
|
||||||
providersStore.deleteProvider(providerId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateReasoning(value: boolean) {
|
async function updateReasoning(value: boolean) {
|
||||||
@@ -104,6 +102,7 @@ async function updateReasoning(value: boolean) {
|
|||||||
:value="metadata.id"
|
:value="metadata.id"
|
||||||
:title="metadata.localizedName || 'Unknown'"
|
:title="metadata.localizedName || 'Unknown'"
|
||||||
:description="metadata.localizedDescription"
|
:description="metadata.localizedDescription"
|
||||||
|
@update:model-value="persistSelection"
|
||||||
@click="trackProviderClick(metadata.id, 'consciousness')"
|
@click="trackProviderClick(metadata.id, 'consciousness')"
|
||||||
>
|
>
|
||||||
<template v-if="!metadata.id.startsWith('official-provider')" #topRight>
|
<template v-if="!metadata.id.startsWith('official-provider')" #topRight>
|
||||||
@@ -194,6 +193,7 @@ async function updateReasoning(value: boolean) {
|
|||||||
v-model="activeModel" type="text"
|
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"
|
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')"
|
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
||||||
|
@input="persistSelection"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -285,6 +285,7 @@ async function updateReasoning(value: boolean) {
|
|||||||
v-model="activeModel" type="text"
|
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"
|
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')"
|
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
||||||
|
@input="persistSelection"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ function selectSpeechProvider(providerId: string) {
|
|||||||
* Tracks explicit voice selection from catalog or custom input controls.
|
* Tracks explicit voice selection from catalog or custom input controls.
|
||||||
*/
|
*/
|
||||||
async function selectSpeechVoice(voiceId: string | undefined) {
|
async function selectSpeechVoice(voiceId: string | undefined) {
|
||||||
|
await persistSelection()
|
||||||
if (!voiceId)
|
if (!voiceId)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -290,11 +291,15 @@ async function selectSpeechVoice(voiceId: string | undefined) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectSpeechSource(sourceId: string) {
|
async function selectSpeechSource(sourceId: string) {
|
||||||
activeSpeechProvider.value = sourceId
|
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 streamingModelId = modelIdFromStreamingOptionId(modelOptionId)
|
||||||
const nextProvider = streamingModelId == null
|
const nextProvider = streamingModelId == null
|
||||||
? activeSpeechProvider.value === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
|
? activeSpeechProvider.value === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
|
||||||
@@ -310,6 +315,9 @@ function selectSpeechModel(modelOptionId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeSpeechModel.value = nextModel
|
activeSpeechModel.value = nextModel
|
||||||
|
activeSpeechVoiceId.value = ''
|
||||||
|
activeSpeechVoice.value = undefined
|
||||||
|
await persistSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -366,26 +374,11 @@ onMounted(async () => {
|
|||||||
trackOfficialTtsExposure()
|
trackOfficialTtsExposure()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(activeSpeechProvider, async (newProvider, oldProvider) => {
|
watch(activeSpeechProvider, async (newProvider) => {
|
||||||
await providersStore.loadModelsForConfiguredProviders()
|
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()
|
speechStore.ensureActiveSpeechModel()
|
||||||
await speechStore.loadVoicesForProvider(newProvider, activeSpeechModel.value || undefined)
|
await speechStore.loadVoicesForProvider(newProvider, activeSpeechModel.value || undefined)
|
||||||
trackOfficialTtsExposure(newProvider, currentTtsModelId())
|
trackOfficialTtsExposure(newProvider, currentTtsModelId())
|
||||||
@@ -397,16 +390,17 @@ watch(activeSpeechModel, async (model) => {
|
|||||||
if (!activeSpeechProvider.value)
|
if (!activeSpeechProvider.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
activeSpeechVoiceId.value = ''
|
|
||||||
activeSpeechVoice.value = undefined
|
|
||||||
|
|
||||||
await speechStore.loadVoicesForProvider(activeSpeechProvider.value, model || undefined)
|
await speechStore.loadVoicesForProvider(activeSpeechProvider.value, model || undefined)
|
||||||
trackOfficialTtsExposure(activeSpeechProvider.value, currentTtsModelId())
|
trackOfficialTtsExposure(activeSpeechProvider.value, currentTtsModelId())
|
||||||
})
|
})
|
||||||
|
|
||||||
watch([activeSpeechProvider, activeSpeechModel, activeSpeechVoiceId], ([provider, model, voiceId]) => {
|
async function persistSelection() {
|
||||||
void airiCardStore.updateActiveCardSpeech({ provider, model, voice_id: voiceId })
|
await airiCardStore.updateActiveCardSpeech({
|
||||||
})
|
provider: activeSpeechProvider.value,
|
||||||
|
model: activeSpeechModel.value,
|
||||||
|
voice_id: activeSpeechVoiceId.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Function to generate speech
|
// Function to generate speech
|
||||||
async function generateTestSpeech() {
|
async function generateTestSpeech() {
|
||||||
@@ -589,21 +583,17 @@ function commitCustomVoiceSelection() {
|
|||||||
|
|
||||||
function updateCustomModelName(value: string | undefined) {
|
function updateCustomModelName(value: string | undefined) {
|
||||||
activeSpeechModel.value = value || ''
|
activeSpeechModel.value = value || ''
|
||||||
|
activeSpeechVoiceId.value = ''
|
||||||
|
void persistSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteProvider(providerId: string) {
|
async function handleDeleteProvider(providerId: string) {
|
||||||
if (providerId === 'speech-noop') {
|
if (providerId === 'speech-noop') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeSpeechProvider.value === providerId) {
|
await airiCardStore.clearProviderSelections(providerId)
|
||||||
activeSpeechProvider.value = 'speech-noop'
|
await providersStore.deleteProvider(providerId)
|
||||||
activeSpeechModel.value = ''
|
|
||||||
activeSpeechVoiceId.value = ''
|
|
||||||
activeSpeechVoice.value = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
providersStore.deleteProvider(providerId)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -899,6 +889,7 @@ function handleDeleteProvider(providerId: string) {
|
|||||||
<select
|
<select
|
||||||
v-model="activeSpeechModel"
|
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"
|
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">
|
<option value="eleven_monolingual_v1">
|
||||||
Monolingual v1
|
Monolingual v1
|
||||||
|
|||||||
@@ -41,31 +41,30 @@ const {
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { trackProviderClick } = useAnalytics()
|
const { trackProviderClick } = useAnalytics()
|
||||||
|
|
||||||
watch(activeProvider, async (provider, oldProvider) => {
|
watch(activeProvider, async (provider) => {
|
||||||
if (!provider)
|
if (!provider)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (oldProvider !== undefined && oldProvider !== provider) {
|
|
||||||
visionStore.resetModelSelection()
|
|
||||||
}
|
|
||||||
|
|
||||||
await visionStore.loadModelsForProvider(provider)
|
await visionStore.loadModelsForProvider(provider)
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
watch([activeProvider, activeModel], ([provider, model]) => {
|
async function selectProvider(provider: string) {
|
||||||
void airiCardStore.updateActiveCardVision({ provider, model })
|
activeProvider.value = provider
|
||||||
})
|
visionStore.resetModelSelection()
|
||||||
|
await persistSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistSelection() {
|
||||||
|
await airiCardStore.updateActiveCardVision({ provider: activeProvider.value, model: activeModel.value })
|
||||||
|
}
|
||||||
|
|
||||||
function updateCustomModelName(value: string) {
|
function updateCustomModelName(value: string) {
|
||||||
customModelName.value = value
|
customModelName.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteProvider(providerId: string) {
|
async function handleDeleteProvider(providerId: string) {
|
||||||
if (activeProvider.value === providerId) {
|
await airiCardStore.clearProviderSelections(providerId)
|
||||||
activeProvider.value = ''
|
await providersStore.deleteProvider(providerId)
|
||||||
activeModel.value = ''
|
|
||||||
}
|
|
||||||
providersStore.deleteProvider(providerId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedLastCapture = computed(() => formatRelativeTime(lastCaptureAt.value))
|
const formattedLastCapture = computed(() => formatRelativeTime(lastCaptureAt.value))
|
||||||
@@ -114,11 +113,12 @@ function formatRelativeTime(timestamp: number | null) {
|
|||||||
v-for="metadata in moduleVisionProvidersMetadata"
|
v-for="metadata in moduleVisionProvidersMetadata"
|
||||||
:id="metadata.id"
|
:id="metadata.id"
|
||||||
:key="metadata.id"
|
:key="metadata.id"
|
||||||
v-model="activeProvider"
|
:model-value="activeProvider"
|
||||||
name="provider"
|
name="provider"
|
||||||
:value="metadata.id"
|
:value="metadata.id"
|
||||||
:title="metadata.localizedName || 'Unknown'"
|
:title="metadata.localizedName || 'Unknown'"
|
||||||
:description="metadata.localizedDescription"
|
:description="metadata.localizedDescription"
|
||||||
|
@update:model-value="selectProvider"
|
||||||
@click="trackProviderClick(metadata.id, 'vision')"
|
@click="trackProviderClick(metadata.id, 'vision')"
|
||||||
>
|
>
|
||||||
<template v-if="canDeleteProvider(metadata.id)" #topRight>
|
<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')"
|
: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')"
|
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
|
||||||
expanded-class="mb-12"
|
expanded-class="mb-12"
|
||||||
|
@update:model-value="persistSelection"
|
||||||
@update:custom-value="updateCustomModelName"
|
@update:custom-value="updateCustomModelName"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -336,6 +337,7 @@ function formatRelativeTime(timestamp: number | null) {
|
|||||||
'dark:bg-neutral-900',
|
'dark:bg-neutral-900',
|
||||||
]"
|
]"
|
||||||
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
:placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.manual_model_placeholder')"
|
||||||
|
@input="persistSelection"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
Shared core for stage
|
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
|
## Button analytics
|
||||||
|
|
||||||
Register the shared plugin once in each Vue application:
|
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 { getAuthToken } from '../../../../libs/auth'
|
||||||
import { SERVER_URL } from '../../../../libs/server'
|
import { SERVER_URL } from '../../../../libs/server'
|
||||||
import { defineProvider } from '../registry'
|
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'
|
import { createOfficialAudioProvider, createOfficialOpenAIProvider, OFFICIAL_ICON, withCredentials } from './shared'
|
||||||
|
|
||||||
export const OFFICIAL_CHAT_PROVIDER_ID = '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'
|
||||||
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'
|
|
||||||
|
|
||||||
// Locale → voice id map recommended by the server, keyed by provider id.
|
// Locale → voice id map recommended by the server, keyed by provider id.
|
||||||
// Populated by each speech provider's listVoices() from the response's
|
// 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 { 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', () => {
|
describe('airi card editor validation', () => {
|
||||||
// https://github.com/moeru-ai/airi/issues/2108
|
// https://github.com/moeru-ai/airi/issues/2108
|
||||||
@@ -70,6 +70,56 @@ describe('airi card editor validation', () => {
|
|||||||
expect(result.output.artistryOptions).toBeUndefined()
|
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', () => {
|
it('preserves AIRI extension fields that are not editable in the form', () => {
|
||||||
// ROOT CAUSE:
|
// 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 & {
|
type CardWithAiriExtension = Card & {
|
||||||
extensions: NonNullable<Card['extensions']> & {
|
extensions: NonNullable<Card['extensions']> & {
|
||||||
airi: AiriExtension
|
airi: AiriExtension
|
||||||
@@ -159,6 +187,21 @@ function isAiriExtension(value: unknown): value is AiriExtension {
|
|||||||
&& isRecord(value.agents)
|
&& 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> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
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')
|
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:
|
// ROOT CAUSE:
|
||||||
//
|
//
|
||||||
// Each Electron window called the synchronized initialize action. The leader
|
// Each Electron window called the synchronized initialize action. The leader
|
||||||
@@ -161,50 +242,6 @@ describe('airi-card store', () => {
|
|||||||
expect(visionStore.activeModel).toBe('auto')
|
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:
|
// ROOT CAUSE:
|
||||||
//
|
//
|
||||||
// A synchronized state snapshot replaced `activeCardId`. The old watcher
|
// A synchronized state snapshot replaced `activeCardId`. The old watcher
|
||||||
@@ -289,17 +326,17 @@ describe('airi-card store', () => {
|
|||||||
const cardStore = useAiriCardStore()
|
const cardStore = useAiriCardStore()
|
||||||
await cardStore.initialize()
|
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.updateActiveCardConsciousness({ provider: 'openrouter-ai', model: 'anthropic/claude-sonnet' })).toBe(true)
|
||||||
expect(await cardStore.updateActiveCardVision({ provider: 'ollama', model: 'llava' })).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(await cardStore.updateActiveCardSpeech({ provider: 'elevenlabs', model: 'eleven_multilingual_v2', voice_id: 'aria' })).toBe(true)
|
||||||
expect(cardStore.activeCard?.extensions.airi.modules).toMatchObject({
|
expect(cardStore.activeCard?.extensions.airi.modules).toMatchObject({
|
||||||
displayModelId: 'display-model-iru-v2',
|
displayModelId: 'preset-vrm-1',
|
||||||
consciousness: { provider: 'openrouter-ai', model: 'anthropic/claude-sonnet' },
|
consciousness: { provider: 'openrouter-ai', model: 'anthropic/claude-sonnet' },
|
||||||
vision: { provider: 'ollama', model: 'llava' },
|
vision: { provider: 'ollama', model: 'llava' },
|
||||||
speech: { provider: 'elevenlabs', model: 'eleven_multilingual_v2', voice_id: 'aria' },
|
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:
|
// ROOT CAUSE:
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import type { Card, ccv3 } from '@proj-airi/ccc'
|
import type { Card, ccv3 } from '@proj-airi/ccc'
|
||||||
|
|
||||||
|
import type { CardModuleDefaults } from '../../services/airi-card-modules'
|
||||||
import type { AiriCard, AiriExtension } from '../../types/airiCard'
|
import type { AiriCard, AiriExtension } from '../../types/airiCard'
|
||||||
|
|
||||||
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
|
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
|
||||||
|
import { StorageSerializers } from '@vueuse/core'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed } from 'vue'
|
import { computed, toRaw } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT } from '../../constants/prompts/character-defaults'
|
import { DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT } from '../../constants/prompts/character-defaults'
|
||||||
import { captureAnalyticsEvent } from '../../libs/product-signals'
|
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 { useSettingsStageModel } from '../settings/stage-model'
|
||||||
import { useArtistryStore } from './artistry'
|
import { useArtistryStore } from './artistry'
|
||||||
import { useConsciousnessStore } from './consciousness'
|
import { useConsciousnessStore } from './consciousness'
|
||||||
|
import { configureAsDefaultsIfEmpty, unconfigureAuthenticationProviders } from './default'
|
||||||
import { useSpeechStore } from './speech'
|
import { useSpeechStore } from './speech'
|
||||||
import { useVisionStore } from './vision'
|
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 })
|
const activeCardId = useLocalStorageManualReset<string>('airi-card-active-id', 'default', { listenToStorageChanges: false })
|
||||||
let initialized = 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))
|
const activeCard = computed(() => cards.value.get(activeCardId.value))
|
||||||
function useRuntimeModuleStores() {
|
function useRuntimeModuleStores() {
|
||||||
return {
|
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
|
* `source` feeds the `card_created` analytics event: `scratch` = built in
|
||||||
* the creation dialog, `import` = ccv3 JSON upload, `duplicate` = cloned
|
* the creation dialog, `import` = ccv3 JSON upload, `duplicate` = cloned
|
||||||
@@ -70,6 +181,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeCard = async (id: string) => {
|
const removeCard = async (id: string) => {
|
||||||
|
await pendingAuthenticationSetup
|
||||||
// The built-in card is the guaranteed fallback for every runtime profile.
|
// The built-in card is the guaranteed fallback for every runtime profile.
|
||||||
if (id === 'default')
|
if (id === 'default')
|
||||||
return false
|
return false
|
||||||
@@ -90,6 +202,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateCard = async (id: string, updates: AiriCard | Card | ccv3.CharacterCardV3) => {
|
const updateCard = async (id: string, updates: AiriCard | Card | ccv3.CharacterCardV3) => {
|
||||||
|
await pendingAuthenticationSetup
|
||||||
const existingCard = cards.value.get(id)
|
const existingCard = cards.value.get(id)
|
||||||
if (!existingCard)
|
if (!existingCard)
|
||||||
return false
|
return false
|
||||||
@@ -136,6 +249,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateActiveCardDisplayModel(displayModelId: string | undefined) {
|
async function updateActiveCardDisplayModel(displayModelId: string | undefined) {
|
||||||
|
await pendingAuthenticationSetup
|
||||||
const updated = updateActiveCardModules(() => ({ displayModelId }))
|
const updated = updateActiveCardModules(() => ({ displayModelId }))
|
||||||
if (updated)
|
if (updated)
|
||||||
applyActiveCardSettings()
|
applyActiveCardSettings()
|
||||||
@@ -143,6 +257,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateActiveCardConsciousness(consciousness: AiriExtension['modules']['consciousness']) {
|
async function updateActiveCardConsciousness(consciousness: AiriExtension['modules']['consciousness']) {
|
||||||
|
await pendingAuthenticationSetup
|
||||||
const updated = updateActiveCardModules(() => ({ consciousness }))
|
const updated = updateActiveCardModules(() => ({ consciousness }))
|
||||||
if (updated)
|
if (updated)
|
||||||
applyActiveCardSettings()
|
applyActiveCardSettings()
|
||||||
@@ -150,6 +265,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateActiveCardVision(vision: AiriExtension['modules']['vision']) {
|
async function updateActiveCardVision(vision: AiriExtension['modules']['vision']) {
|
||||||
|
await pendingAuthenticationSetup
|
||||||
const updated = updateActiveCardModules(() => ({ vision }))
|
const updated = updateActiveCardModules(() => ({ vision }))
|
||||||
if (updated)
|
if (updated)
|
||||||
applyActiveCardSettings()
|
applyActiveCardSettings()
|
||||||
@@ -157,6 +273,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateActiveCardSpeech(speech: Pick<AiriExtension['modules']['speech'], 'provider' | 'model' | 'voice_id'>) {
|
async function updateActiveCardSpeech(speech: Pick<AiriExtension['modules']['speech'], 'provider' | 'model' | 'voice_id'>) {
|
||||||
|
await pendingAuthenticationSetup
|
||||||
const updated = updateActiveCardModules(({ modules }) => ({
|
const updated = updateActiveCardModules(({ modules }) => ({
|
||||||
speech: {
|
speech: {
|
||||||
...modules.speech,
|
...modules.speech,
|
||||||
@@ -168,63 +285,33 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Clears a removed provider from defaults and the active card, not other cards. */
|
||||||
* Persists the current inference selections in the active card.
|
async function clearProviderSelections(providerId: string) {
|
||||||
*
|
await pendingAuthenticationSetup
|
||||||
* This command snapshots runtime state after a higher-level operation, such
|
rememberInheritedSettings()
|
||||||
* as authenticated default setup. It deliberately does not apply the card
|
const defaults = moduleDefaults.value
|
||||||
* back to the runtime, so one persistence write cannot start another module
|
if (!defaults)
|
||||||
* transition.
|
return
|
||||||
*/
|
const next = structuredClone(toRaw(defaults))
|
||||||
async function persistActiveCardModuleSelections() {
|
for (const module of ['consciousness', 'vision', 'speech'] as const) {
|
||||||
const card = cards.value.get(activeCardId.value)
|
if (next[module].provider === providerId) {
|
||||||
if (!card)
|
next[module].provider = module === 'speech' ? 'speech-noop' : ''
|
||||||
return false
|
next[module].model = ''
|
||||||
|
if (module === 'speech')
|
||||||
const {
|
next.speech.voice_id = ''
|
||||||
consciousness,
|
}
|
||||||
speech,
|
}
|
||||||
vision,
|
moduleDefaults.value = next
|
||||||
} = useRuntimeModuleStores()
|
updateActiveCardModules(({ modules }) => ({
|
||||||
const modules = card.extensions?.airi?.modules
|
consciousness: modules.consciousness.provider === providerId ? { provider: '', model: '' } : modules.consciousness,
|
||||||
const alreadyPersisted = modules?.consciousness?.provider === consciousness.activeProvider
|
vision: modules.vision.provider === providerId ? { provider: '', model: '' } : modules.vision,
|
||||||
&& modules.consciousness.model === consciousness.activeModel
|
speech: modules.speech.provider === providerId ? { ...modules.speech, provider: '', model: '', voice_id: '' } : modules.speech,
|
||||||
&& 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,
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
appliedModules = undefined
|
||||||
|
applyActiveCardSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAiriExtension(card: Card | ccv3.CharacterCardV3): AiriExtension {
|
function resolveAiriExtension(card: Card | ccv3.CharacterCardV3): AiriExtension {
|
||||||
const {
|
|
||||||
artistry,
|
|
||||||
consciousness,
|
|
||||||
speech,
|
|
||||||
stageModel,
|
|
||||||
vision,
|
|
||||||
} = useRuntimeModuleStores()
|
|
||||||
|
|
||||||
// Get existing extension if available
|
// Get existing extension if available
|
||||||
const existingExtension = ('data' in card
|
const existingExtension = ('data' in card
|
||||||
? card.data?.extensions?.airi
|
? card.data?.extensions?.airi
|
||||||
@@ -232,28 +319,18 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
|
|
||||||
// Create default modules config
|
// Create default modules config
|
||||||
const defaultModules = {
|
const defaultModules = {
|
||||||
consciousness: {
|
consciousness: { provider: '', model: '' },
|
||||||
provider: consciousness.activeProvider,
|
vision: { provider: '', model: '' },
|
||||||
model: consciousness.activeModel,
|
speech: { provider: '', model: '', voice_id: '' },
|
||||||
},
|
displayModelId: '',
|
||||||
vision: {
|
|
||||||
provider: vision.activeProvider,
|
|
||||||
model: vision.activeModel,
|
|
||||||
},
|
|
||||||
speech: {
|
|
||||||
provider: speech.activeSpeechProvider,
|
|
||||||
model: speech.activeSpeechModel,
|
|
||||||
voice_id: speech.activeSpeechVoiceId,
|
|
||||||
},
|
|
||||||
displayModelId: stageModel.stageModelSelected,
|
|
||||||
artistry: {
|
artistry: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
provider: artistry.globalProvider,
|
provider: '',
|
||||||
model: artistry.globalModel,
|
model: '',
|
||||||
promptPrefix: artistry.globalPromptPrefix,
|
promptPrefix: '',
|
||||||
widgetInstruction: DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT,
|
widgetInstruction: DEFAULT_ARTISTRY_WIDGET_SPAWNING_PROMPT,
|
||||||
spawnMode: 'bg_widget' as const,
|
spawnMode: 'bg_widget' as const,
|
||||||
options: artistry.globalProviderOptions,
|
options: undefined,
|
||||||
autonomousEnabled: false,
|
autonomousEnabled: false,
|
||||||
autonomousThreshold: 70,
|
autonomousThreshold: 70,
|
||||||
autonomousTarget: 'assistant' as const,
|
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 {
|
return {
|
||||||
|
...existingExtension,
|
||||||
modules: {
|
modules: {
|
||||||
|
...existingExtension.modules,
|
||||||
consciousness: {
|
consciousness: {
|
||||||
|
...existingExtension.modules?.consciousness,
|
||||||
provider: existingExtension.modules?.consciousness?.provider ?? defaultModules.consciousness.provider,
|
provider: existingExtension.modules?.consciousness?.provider ?? defaultModules.consciousness.provider,
|
||||||
model: existingExtension.modules?.consciousness?.model ?? defaultModules.consciousness.model,
|
model: existingExtension.modules?.consciousness?.model ?? defaultModules.consciousness.model,
|
||||||
},
|
},
|
||||||
vision: {
|
vision: {
|
||||||
|
...existingExtension.modules?.vision,
|
||||||
provider: existingExtension.modules?.vision?.provider ?? defaultModules.vision.provider,
|
provider: existingExtension.modules?.vision?.provider ?? defaultModules.vision.provider,
|
||||||
model: existingExtension.modules?.vision?.model ?? defaultModules.vision.model,
|
model: existingExtension.modules?.vision?.model ?? defaultModules.vision.model,
|
||||||
},
|
},
|
||||||
speech: {
|
speech: {
|
||||||
|
...existingExtension.modules?.speech,
|
||||||
provider: existingExtension.modules?.speech?.provider ?? defaultModules.speech.provider,
|
provider: existingExtension.modules?.speech?.provider ?? defaultModules.speech.provider,
|
||||||
model: existingExtension.modules?.speech?.model ?? defaultModules.speech.model,
|
model: existingExtension.modules?.speech?.model ?? defaultModules.speech.model,
|
||||||
voice_id: existingExtension.modules?.speech?.voice_id ?? defaultModules.speech.voice_id,
|
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,
|
displayModelId: existingExtension.modules?.displayModelId ?? defaultModules.displayModelId,
|
||||||
activeBackgroundId: existingExtension.modules?.activeBackgroundId,
|
activeBackgroundId: existingExtension.modules?.activeBackgroundId,
|
||||||
artistry: {
|
artistry: {
|
||||||
|
...existingExtension.modules?.artistry,
|
||||||
enabled: existingExtension.modules?.artistry?.enabled ?? (existingExtension as any).artistry?.enabled ?? defaultModules.artistry.enabled,
|
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,
|
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,
|
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 ?? [],
|
tags: ccv3Card.data.tags ?? [],
|
||||||
extensions: {
|
extensions: {
|
||||||
airi: resolveAiriExtension(ccv3Card),
|
|
||||||
...ccv3Card.data.extensions,
|
...ccv3Card.data.extensions,
|
||||||
|
airi: resolveAiriExtension(ccv3Card),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,13 +435,14 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
return {
|
return {
|
||||||
...card,
|
...card,
|
||||||
extensions: {
|
extensions: {
|
||||||
airi: resolveAiriExtension(card),
|
|
||||||
...card.extensions,
|
...card.extensions,
|
||||||
|
airi: resolveAiriExtension(card),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
|
await pendingAuthenticationSetup
|
||||||
// This synchronized action executes in the leader. Each window calls it,
|
// This synchronized action executes in the leader. Each window calls it,
|
||||||
// but only the first call can apply persisted card settings to the runtime.
|
// but only the first call can apply persisted card settings to the runtime.
|
||||||
if (initialized)
|
if (initialized)
|
||||||
@@ -366,12 +450,26 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
|
|
||||||
initialized = true
|
initialized = true
|
||||||
if (!cards.value.has('default')) {
|
if (!cards.value.has('default')) {
|
||||||
cards.value.set('default', newAiriCard({
|
const defaultCard: AiriCard = {
|
||||||
name: 'ReLU',
|
name: 'ReLU',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
description: t('base.prompt.prefix'),
|
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
|
// The active id and card map are persisted separately. Older versions
|
||||||
// could delete the selected card without repairing its stored id.
|
// could delete the selected card without repairing its stored id.
|
||||||
if (!cards.value.has(activeCardId.value))
|
if (!cards.value.has(activeCardId.value))
|
||||||
@@ -385,6 +483,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
* leader. Replicated state snapshots never invoke this command.
|
* leader. Replicated state snapshots never invoke this command.
|
||||||
*/
|
*/
|
||||||
async function activateCard(id: string) {
|
async function activateCard(id: string) {
|
||||||
|
await pendingAuthenticationSetup
|
||||||
if (!cards.value.has(id))
|
if (!cards.value.has(id))
|
||||||
return false
|
return false
|
||||||
|
|
||||||
@@ -394,13 +493,8 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyActiveCardSettings(newCard = activeCard.value) {
|
function applyActiveCardSettings(newCard = activeCard.value) {
|
||||||
const {
|
rememberInheritedSettings()
|
||||||
artistry,
|
const artistry = useArtistryStore()
|
||||||
consciousness,
|
|
||||||
speech,
|
|
||||||
stageModel,
|
|
||||||
vision,
|
|
||||||
} = useRuntimeModuleStores()
|
|
||||||
|
|
||||||
artistry.resetToGlobal()
|
artistry.resetToGlobal()
|
||||||
|
|
||||||
@@ -412,38 +506,47 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
if (!extension)
|
if (!extension)
|
||||||
return
|
return
|
||||||
|
|
||||||
const consciousnessSettings = extension.modules?.consciousness
|
const defaults = moduleDefaults.value
|
||||||
if (consciousnessSettings?.provider)
|
if (!defaults)
|
||||||
consciousness.activeProvider = consciousnessSettings.provider
|
return
|
||||||
if (consciousnessSettings?.model)
|
const modules = extension.modules
|
||||||
consciousness.activeModel = consciousnessSettings.model
|
const speechSelection = resolveModuleSelection(modules.speech, defaults.speech)
|
||||||
|
const resolved: CardModuleDefaults = {
|
||||||
const visionSettings = extension.modules?.vision
|
consciousness: resolveModuleSelection(modules.consciousness, defaults.consciousness),
|
||||||
if (visionSettings?.provider)
|
vision: resolveModuleSelection(modules.vision, defaults.vision),
|
||||||
vision.activeProvider = visionSettings.provider
|
speech: {
|
||||||
if (visionSettings?.model)
|
...speechSelection,
|
||||||
vision.activeModel = visionSettings.model
|
voice_id: modules.speech.voice_id || (
|
||||||
|
speechSelection.provider === defaults.speech.provider && speechSelection.model === defaults.speech.model
|
||||||
const speechSettings = extension.modules?.speech
|
? defaults.speech.voice_id
|
||||||
if (speechSettings?.provider)
|
: ''
|
||||||
speech.activeSpeechProvider = speechSettings.provider
|
),
|
||||||
if (speechSettings?.model)
|
},
|
||||||
speech.activeSpeechModel = speechSettings.model
|
displayModelId: modules.displayModelId || defaults.displayModelId,
|
||||||
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 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) {
|
||||||
if (extension.modules.artistry.provider)
|
const selection = resolveModuleSelection({
|
||||||
artistry.activeProvider = extension.modules.artistry.provider
|
provider: extension.modules.artistry.provider ?? '',
|
||||||
if (extension.modules.artistry.model)
|
model: extension.modules.artistry.model ?? '',
|
||||||
artistry.activeModel = 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)
|
if (extension.modules.artistry.promptPrefix)
|
||||||
artistry.defaultPromptPrefix = extension.modules.artistry.promptPrefix
|
artistry.defaultPromptPrefix = extension.modules.artistry.promptPrefix
|
||||||
if (extension.modules.artistry.options)
|
if (extension.modules.artistry.options)
|
||||||
@@ -453,12 +556,15 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
initialized = false
|
initialized = false
|
||||||
|
appliedModules = undefined
|
||||||
|
moduleDefaults.reset()
|
||||||
cards.reset()
|
cards.reset()
|
||||||
activeCardId.reset()
|
activeCardId.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cards,
|
cards,
|
||||||
|
moduleDefaults,
|
||||||
activeCard,
|
activeCard,
|
||||||
activeCardId,
|
activeCardId,
|
||||||
addCard,
|
addCard,
|
||||||
@@ -466,13 +572,14 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
updateCard,
|
updateCard,
|
||||||
updateActiveCardConsciousness,
|
updateActiveCardConsciousness,
|
||||||
updateActiveCardDisplayModel,
|
updateActiveCardDisplayModel,
|
||||||
persistActiveCardModuleSelections,
|
|
||||||
updateActiveCardSpeech,
|
updateActiveCardSpeech,
|
||||||
updateActiveCardVision,
|
updateActiveCardVision,
|
||||||
getCard,
|
getCard,
|
||||||
resetState,
|
resetState,
|
||||||
initialize,
|
initialize,
|
||||||
activateCard,
|
activateCard,
|
||||||
|
configureForAuthentication,
|
||||||
|
clearProviderSelections,
|
||||||
|
|
||||||
currentModels: computed(() => {
|
currentModels: computed(() => {
|
||||||
const {
|
const {
|
||||||
@@ -508,8 +615,9 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
|||||||
'activateCard',
|
'activateCard',
|
||||||
'addCard',
|
'addCard',
|
||||||
'initialize',
|
'initialize',
|
||||||
|
'configureForAuthentication',
|
||||||
|
'clearProviderSelections',
|
||||||
'removeCard',
|
'removeCard',
|
||||||
'persistActiveCardModuleSelections',
|
|
||||||
'updateActiveCardConsciousness',
|
'updateActiveCardConsciousness',
|
||||||
'updateActiveCardDisplayModel',
|
'updateActiveCardDisplayModel',
|
||||||
'updateActiveCardSpeech',
|
'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 { useProviderConfigStore } from '../providers/config'
|
||||||
import { useProviderStore } from '../providers/provider'
|
import { useProviderStore } from '../providers/provider'
|
||||||
import { useConsciousnessStore } from './consciousness'
|
import { useConsciousnessStore } from './consciousness'
|
||||||
|
|||||||
Reference in New Issue
Block a user