diff --git a/apps/stage-web/src/pages/settings/airi-card/components/CardDetailDialog.vue b/apps/stage-web/src/pages/settings/airi-card/components/CardDetailDialog.vue index a904406d2..40fc0c161 100644 --- a/apps/stage-web/src/pages/settings/airi-card/components/CardDetailDialog.vue +++ b/apps/stage-web/src/pages/settings/airi-card/components/CardDetailDialog.vue @@ -16,6 +16,7 @@ import { import { computed, ref } from 'vue' import { useI18n } from 'vue-i18n' +import CardEditDialog from './CardEditDialog.vue' import DeleteCardDialog from './DeleteCardDialog.vue' interface Props { @@ -77,6 +78,9 @@ const isActive = computed(() => props.cardId === activeCardId.value) // Animation control for card activation const isActivating = ref(false) +// Edit dialog state +const isEditDialogOpen = ref(false) + function handleActivate() { isActivating.value = true setTimeout(() => { @@ -194,6 +198,14 @@ const activeTab = computed({
+ +
@@ -344,6 +364,12 @@ const activeTab = computed({ + + + +import type { Card } from '@proj-airi/ccc' + +import kebabcase from '@stdlib/string-base-kebabcase' + +import { Button } from '@proj-airi/stage-ui/components' +import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card' +import { FieldValues } from '@proj-airi/ui' +import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui' +import { computed, onMounted, ref, toRaw, watch } from 'vue' +import { useI18n } from 'vue-i18n' + +interface Props { + modelValue: boolean + cardId: string +} + +const props = defineProps() +const emit = defineEmits<{ + (e: 'update:modelValue', value: boolean): void +}>() + +// Use computed for two-way binding +const modelValue = computed({ + get: () => props.modelValue, + set: value => emit('update:modelValue', value), +}) + +const cardId = computed(() => props.cardId) + +const { t } = useI18n() +const cardStore = useAiriCardStore() + +// Tab type definition +interface Tab { + id: string + label: string + icon: string +} + +// Active tab ID state +const activeTabId = ref('') + +// Tabs for card details +const tabs: Tab[] = [ + { id: 'identity', label: t('settings.pages.card.creation.identity'), icon: 'i-solar:emoji-funny-square-bold-duotone' }, + { id: 'behavior', label: t('settings.pages.card.creation.behavior'), icon: 'i-solar:chat-round-line-bold-duotone' }, + { id: 'settings', label: t('settings.pages.card.creation.settings'), icon: 'i-solar:settings-bold-duotone' }, +] + +// Active tab state - set to first available tab by default +const activeTab = computed({ + get: () => { + // If current active tab is not in available tabs, reset to first tab + if (!tabs.find(tab => tab.id === activeTabId.value)) + return tabs[0]?.id || '' + return activeTabId.value + }, + set: (value: string) => { + activeTabId.value = value + }, +}) + +// Check for errors, and save built Cards : + +const showError = ref(false) +const errorMessage = ref('') + +function saveCard(card: Card): boolean { + // Before saving, let's validate what the user entered : + const rawCard: Card = toRaw(card) + + if (!(rawCard.name!.length > 0)) { + // No name + showError.value = true + errorMessage.value = t('settings.pages.card.creation.errors.name') + return false + } + else if (!/^(?:\d+\.)+\d+$/.test(rawCard.version)) { + // Invalid version + showError.value = true + errorMessage.value = t('settings.pages.card.creation.errors.version') + return false + } + else if (!(rawCard.description!.length > 0)) { + // No description + showError.value = true + errorMessage.value = t('settings.pages.card.creation.errors.description') + return false + } + else if (!(rawCard.personality!.length > 0)) { + // No personality + showError.value = true + errorMessage.value = t('settings.pages.card.creation.errors.personality') + return false + } + else if (!(rawCard.scenario!.length > 0)) { + // No Scenario + showError.value = true + errorMessage.value = t('settings.pages.card.creation.errors.scenario') + return false + } + else if (!(rawCard.systemPrompt!.length > 0)) { + // No sys prompt + showError.value = true + errorMessage.value = t('settings.pages.card.creation.errors.systemprompt') + return false + } + else if (!(rawCard.postHistoryInstructions!.length > 0)) { + // No post history prompt + showError.value = true + errorMessage.value = t('settings.pages.card.creation.errors.posthistoryinstructions') + return false + } + showError.value = false + + // Update the card in the store + const success = cardStore.updateCard(cardId.value, rawCard) + if (success) { + modelValue.value = false // Close the dialog + } + return success +} + +// Cards data holders : +const card = ref({ + name: '', + nickname: undefined, + version: '1.0', + description: '', + notes: undefined, + personality: '', + scenario: '', + systemPrompt: '', + postHistoryInstructions: '', + greetings: [], + messageExample: [], +}) + +// Initialize card data when component mounts, cardId changes, or dialog opens +onMounted(() => { + if (modelValue.value && cardId.value) { + loadCardData() + } +}) + +// Watch both cardId and modelValue to ensure data loads when dialog opens +watch([() => props.cardId, () => modelValue.value], ([newCardId, isOpen]) => { + if (isOpen && newCardId) { + loadCardData() + } +}) + +function loadCardData() { + const existingCard = cardStore.getCard(cardId.value) + if (existingCard) { + // Deep clone to avoid modifying the store directly + // Ensure all required properties are correctly assigned + card.value = { + name: existingCard.name || '', + nickname: existingCard.nickname, + version: existingCard.version || '1.0', + description: existingCard.description || '', + notes: existingCard.notes, + personality: existingCard.personality || '', + scenario: existingCard.scenario || '', + systemPrompt: existingCard.systemPrompt || '', + postHistoryInstructions: existingCard.postHistoryInstructions || '', + greetings: [...(existingCard.greetings || [])], + messageExample: [...(existingCard.messageExample || [])], + } + } +} + +function makeComputed( + /* + Function used to generate Computed values, with an optional sanitize function + */ + key: T, + transform?: (input: string) => string, +) { + return computed({ + get: () => { + return card.value[key] ?? '' + }, + set: (val: string) => { // Set, + const input = val.trim() // We first trim the value + card.value[key] = (input.length > 0 + ? (transform ? transform(input) : input) // then potentially transform it + : '') as Card[T]// or default to empty string value if nothing was given + }, + }) +} + +const cardName = makeComputed('name', input => kebabcase(input)) +const cardNickname = makeComputed('nickname') +const cardDescription = makeComputed('description') +const cardNotes = makeComputed('notes') + +const cardPersonality = makeComputed('personality') +const cardScenario = makeComputed('scenario') +const cardGreetings = computed({ + get: () => card.value.greetings ?? [], + set: (val: string[]) => { + card.value.greetings = val || [] + }, +}) + +const cardVersion = makeComputed('version') +const cardSystemPrompt = makeComputed('systemPrompt') +const cardPostHistoryInstructions = makeComputed('postHistoryInstructions') + + +