feat(stage-ui): add edit mode for airi card (#939)

This commit is contained in:
leafyy
2026-01-12 18:53:14 +08:00
committed by GitHub
parent a1ba336d7e
commit cf8811d5eb
5 changed files with 106 additions and 20 deletions
@@ -75,6 +75,7 @@ pages:
active: Active
active_badge: Currently Active
cancel: Cancel action
save: Save changes
card_not_found: Card not found
character: Character
close: Close
@@ -89,6 +90,7 @@ pages:
description_label: Description
drop_here: Drop to upload
create_card: Create a new Card
edit_card: Edit Card
creation:
identity: Identity
name: Name
@@ -12,14 +12,15 @@ import {
DialogRoot,
DialogTitle,
} from 'reka-ui'
import { computed, ref, toRaw } from 'vue'
import { computed, ref, toRaw, watch } from 'vue'
import { useI18n } from 'vue-i18n'
interface Props {
modelValue: boolean
cardId?: string // If provided, edit mode; otherwise create mode
}
defineProps<Props>()
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
@@ -29,6 +30,9 @@ const modelValue = defineModel<boolean>()
const { t } = useI18n()
const cardStore = useAiriCardStore()
// Determine if we're in edit mode
const isEditMode = computed(() => !!props.cardId)
// Tab type definition
interface Tab {
id: string
@@ -112,25 +116,53 @@ function saveCard(card: Card): boolean {
}
showError.value = false
cardStore.addCard(rawCard)
if (isEditMode.value && props.cardId) {
// Edit mode: update existing card
cardStore.updateCard(props.cardId, rawCard)
}
else {
// Create mode: add new card
cardStore.addCard(rawCard)
}
modelValue.value = false // Close this
return true
}
// Cards data holders :
const card = ref<Card>({
name: t('settings.pages.card.creation.defaults.name'),
nickname: undefined,
version: '1.0',
description: '',
notes: undefined,
personality: t('settings.pages.card.creation.defaults.personality'),
scenario: t('settings.pages.card.creation.defaults.scenario'),
systemPrompt: t('settings.pages.card.creation.defaults.systemprompt'),
postHistoryInstructions: t('settings.pages.card.creation.defaults.posthistoryinstructions'),
greetings: [],
messageExample: [],
// Initialize card data - load from existing card if in edit mode
function initializeCard(): Card {
if (isEditMode.value && props.cardId) {
const existingCard = cardStore.getCard(props.cardId)
if (existingCard) {
return { ...toRaw(existingCard) }
}
}
// Default values for create mode
return {
name: t('settings.pages.card.creation.defaults.name'),
nickname: undefined,
version: '1.0',
description: '',
notes: undefined,
personality: t('settings.pages.card.creation.defaults.personality'),
scenario: t('settings.pages.card.creation.defaults.scenario'),
systemPrompt: t('settings.pages.card.creation.defaults.systemprompt'),
postHistoryInstructions: t('settings.pages.card.creation.defaults.posthistoryinstructions'),
greetings: [],
messageExample: [],
}
}
const card = ref<Card>(initializeCard())
// Reinitialize when cardId changes or dialog opens
watch(() => [props.modelValue, props.cardId], () => {
if (props.modelValue) {
card.value = initializeCard()
}
})
function makeComputed<T extends keyof Card>(
@@ -179,7 +211,7 @@ const cardPostHistoryInstructions = makeComputed('postHistoryInstructions')
<DialogContent class="fixed left-1/2 top-1/2 z-100 m-0 max-h-[90vh] max-w-6xl w-[92vw] flex flex-col overflow-auto border border-neutral-200 rounded-xl bg-white p-5 shadow-xl 2xl:w-[60vw] lg:w-[80vw] md:w-[85vw] xl:w-[70vw] -translate-x-1/2 -translate-y-1/2 data-[state=closed]:animate-contentHide data-[state=open]:animate-contentShow dark:border-neutral-700 dark:bg-neutral-800 sm:p-6">
<div class="w-full flex flex-col gap-5">
<DialogTitle text-2xl font-normal class="from-primary-500 to-primary-400 bg-gradient-to-r bg-clip-text text-transparent">
{{ t("settings.pages.card.create_card") }}
{{ isEditMode ? t("settings.pages.card.edit_card") : t("settings.pages.card.create_card") }}
</DialogTitle>
<!-- Dialog tabs -->
@@ -255,7 +287,7 @@ const cardPostHistoryInstructions = makeComputed('postHistoryInstructions')
<Button
variant="primary"
icon="i-solar:check-circle-bold-duotone"
:label="t('settings.pages.card.creation.create')"
:label="isEditMode ? t('settings.pages.card.save') : t('settings.pages.card.creation.create')"
:disabled="false"
@click="saveCard(card)"
/>
@@ -17,6 +17,7 @@ const emit = defineEmits<{
(e: 'select'): void
(e: 'activate'): void
(e: 'delete'): void
(e: 'edit'): void
}>()
</script>
@@ -47,8 +48,17 @@ const emit = defineEmits<{
<h3 flex-1 truncate text-lg font-normal>
{{ name }}
</h3>
<div v-if="isActive" shrink-0 rounded-md p-1 bg="primary-100 dark:primary-900/40" text="primary-600 dark:primary-400">
<div i-solar:check-circle-bold-duotone text-sm />
<div flex shrink-0 items-center gap-2>
<button
rounded-lg p-1 text-neutral-500 transition-colors dark:text-neutral-400 hover="bg-neutral-200 dark:bg-neutral-700/50"
title="Edit card"
@click.stop="emit('edit')"
>
<div i-solar:pen-2-bold-duotone text-sm />
</button>
<div v-if="isActive" rounded-md p-1 bg="primary-100 dark:primary-900/40" text="primary-600 dark:primary-400">
<div i-solar:check-circle-bold-duotone text-sm />
</div>
</div>
</div>
@@ -22,6 +22,8 @@ const { cards, activeCardId } = storeToRefs(cardStore)
// Currently selected card ID (different from active card ID)
const selectedCardId = ref<string>('')
// Currently editing card ID
const editingCardId = ref<string>('')
// Dialog state
const isCardDialogOpen = ref(false)
const isCardCreationDialogOpen = ref(false)
@@ -116,11 +118,27 @@ function confirmDelete(id: string) {
}
function handleSelectCard(cardId: string) {
// Verify card exists before opening dialog
if (!cards.value.has(cardId)) {
console.error(`Card with id ${cardId} not found`)
return
}
selectedCardId.value = cardId
isCardDialogOpen.value = true
}
function handleEditCard(cardId: string) {
// Verify card exists before opening edit dialog
if (!cards.value.has(cardId)) {
console.error(`Card with id ${cardId} not found`)
return
}
editingCardId.value = cardId
isCardCreationDialogOpen.value = true
}
function handleCardCreationDialog() {
editingCardId.value = '' // Clear editing state for new card creation
isCardCreationDialogOpen.value = true
}
@@ -129,6 +147,13 @@ function activateCard(id: string) {
activeCardId.value = id
}
// Clear editing state when creation/edit dialog closes
watch(isCardCreationDialogOpen, (isOpen) => {
if (!isOpen) {
editingCardId.value = ''
}
})
// Card version number
function getVersionNumber(id: string) {
const card = cards.value.get(id)
@@ -241,6 +266,7 @@ function getModuleShortName(id: string, module: 'consciousness' | 'voice') {
@select="handleSelectCard(item.id)"
@activate="activateCard(item.id)"
@delete="confirmDelete(item.id)"
@edit="handleEditCard(item.id)"
/>
</template>
@@ -281,9 +307,10 @@ function getModuleShortName(id: string, module: 'consciousness' | 'voice') {
:card-id="selectedCardId"
/>
<!-- Card detail dialog -->
<!-- Card creation/edit dialog -->
<CardCreationDialog
v-model="isCardCreationDialogOpen"
:card-id="editingCardId"
/>
<!-- Background decoration -->
@@ -84,6 +84,20 @@ export const useAiriCardStore = defineStore('airi-card', () => {
cards.value.delete(id)
}
const updateCard = (id: string, updates: AiriCard | Card | ccv3.CharacterCardV3) => {
const existingCard = cards.value.get(id)
if (!existingCard)
return false
const updatedCard = {
...existingCard,
...updates,
}
cards.value.set(id, newAiriCard(updatedCard))
return true
}
const getCard = (id: string) => {
return cards.value.get(id)
}
@@ -223,6 +237,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
activeCardId,
addCard,
removeCard,
updateCard,
getCard,
resetState,
initialize,