refactor: airi card (#105)
* fix: card icon * style: icon * refactor: use section component * chore: i18n * feat: card detail page * fix: chat
This commit is contained in:
@@ -89,7 +89,11 @@ settings:
|
|||||||
systemprompt: System Prompt
|
systemprompt: System Prompt
|
||||||
posthistoryinstructions: Post-History Instructions
|
posthistoryinstructions: Post-History Instructions
|
||||||
modules: Modules
|
modules: Modules
|
||||||
voice_id: Voice ID
|
consciousness:
|
||||||
|
model: Consciousness / Model
|
||||||
|
speech:
|
||||||
|
model: Speech / Model
|
||||||
|
voice: Speech / Voice
|
||||||
cancel: Cancel
|
cancel: Cancel
|
||||||
memory:
|
memory:
|
||||||
description: Where memories got stored, and organized
|
description: Where memories got stored, and organized
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ settings:
|
|||||||
card:
|
card:
|
||||||
title: Airi 角色卡
|
title: Airi 角色卡
|
||||||
description: 使用 Airi 角色卡预设
|
description: 使用 Airi 角色卡预设
|
||||||
voice_id: 声音 ID
|
|
||||||
upload: 上传
|
upload: 上传
|
||||||
delete: 删除
|
delete: 删除
|
||||||
active: 已激活
|
active: 已激活
|
||||||
@@ -77,6 +76,11 @@ settings:
|
|||||||
posthistoryinstructions: 历史提示指令
|
posthistoryinstructions: 历史提示指令
|
||||||
modules: 模块
|
modules: 模块
|
||||||
cancel: 取消
|
cancel: 取消
|
||||||
|
consciousness:
|
||||||
|
model: 意识 / 模型
|
||||||
|
speech:
|
||||||
|
model: 声音 / 模型
|
||||||
|
voice: 声音 / 声线
|
||||||
memory:
|
memory:
|
||||||
description: 存放记忆的地方,以及策略
|
description: 存放记忆的地方,以及策略
|
||||||
title: 记忆体
|
title: 记忆体
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const model = defineModel<boolean>()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
class="setting-bar cursor-pointer text-sm"
|
class="w-full flex cursor-pointer items-center justify-between rounded-lg px-4 py-3 text-sm outline-none transition-all duration-250 ease-in-out"
|
||||||
bg="neutral-50 dark:neutral-800"
|
bg="neutral-50 dark:neutral-800"
|
||||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
title: string
|
|
||||||
icon: string
|
|
||||||
innerClass?: string
|
|
||||||
expand?: boolean
|
|
||||||
}>(), {
|
|
||||||
expand: true,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Collapsable :default="expand">
|
|
||||||
<template #trigger="slotProps">
|
|
||||||
<button
|
|
||||||
class="setting-bar"
|
|
||||||
bg="zinc-100 dark:zinc-800"
|
|
||||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
|
||||||
@click="slotProps.setVisible(!slotProps.visible)"
|
|
||||||
>
|
|
||||||
<div flex gap-1.5>
|
|
||||||
<div :class="icon" size-6 />
|
|
||||||
{{ $t(title) }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
i-solar:alt-arrow-down-bold-duotone
|
|
||||||
transition="transform duration-250"
|
|
||||||
:class="{ 'rotate-180': slotProps.visible }"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<div grid gap-4 p-4 :class="innerClass">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</Collapsable>
|
|
||||||
</template>
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Section } from '@proj-airi/stage-ui/components'
|
||||||
import { Emotion, EmotionNeutralMotionName } from '@proj-airi/stage-ui/constants'
|
import { Emotion, EmotionNeutralMotionName } from '@proj-airi/stage-ui/constants'
|
||||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||||
import { useFileDialog, useObjectUrl } from '@vueuse/core'
|
import { useFileDialog, useObjectUrl } from '@vueuse/core'
|
||||||
@@ -10,7 +11,6 @@ import { useI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
import ColorPalette from '../Settings/ColorPalette.vue'
|
import ColorPalette from '../Settings/ColorPalette.vue'
|
||||||
import Button from '../Settings/Live2DModelControlButton.vue'
|
import Button from '../Settings/Live2DModelControlButton.vue'
|
||||||
import Section from '../Settings/Section.vue'
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
palette: string[]
|
palette: string[]
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AiriCard } from '@proj-airi/stage-ui/stores'
|
||||||
|
|
||||||
|
import { Button, Section } from '@proj-airi/stage-ui/components'
|
||||||
|
import { useAiriCardStore } from '@proj-airi/stage-ui/stores'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import {
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogRoot,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute('/settings/airi-card/[id]')
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const cardStore = useAiriCardStore()
|
||||||
|
const { getCard, removeCard } = cardStore
|
||||||
|
const { activeCardId } = storeToRefs(cardStore)
|
||||||
|
|
||||||
|
const id = computed(() => route.params.id)
|
||||||
|
|
||||||
|
// Get current card
|
||||||
|
const card = computed<AiriCard | undefined>(() => getCard(id.value as string))
|
||||||
|
|
||||||
|
// Get module settings
|
||||||
|
const moduleSettings = computed(() => {
|
||||||
|
const airiExt = card.value?.extensions?.airi?.modules
|
||||||
|
return {
|
||||||
|
consciousness: airiExt?.consciousness?.model || '',
|
||||||
|
speech: airiExt?.speech?.model || '',
|
||||||
|
voice: airiExt?.speech?.voice_id || '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get character settings
|
||||||
|
const characterSettings = computed(() => {
|
||||||
|
if (!card.value)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
personality: card.value.personality,
|
||||||
|
scenario: card.value.scenario,
|
||||||
|
systemPrompt: card.value.systemPrompt,
|
||||||
|
postHistoryInstructions: card.value.postHistoryInstructions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if card is active
|
||||||
|
const isActive = computed(() => id.value === activeCardId.value)
|
||||||
|
|
||||||
|
// Activate card
|
||||||
|
function activateCard() {
|
||||||
|
activeCardId.value = id.value as string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation control for card activation
|
||||||
|
const isActivating = ref(false)
|
||||||
|
|
||||||
|
function handleActivate() {
|
||||||
|
isActivating.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
activateCard()
|
||||||
|
isActivating.value = false
|
||||||
|
activeCardId.value = 'default'
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete card confirmation
|
||||||
|
const showDeleteConfirm = ref(false)
|
||||||
|
|
||||||
|
function handleDeleteConfirm() {
|
||||||
|
if (card.value) {
|
||||||
|
removeCard(id.value as string)
|
||||||
|
activateCard()
|
||||||
|
}
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function hightlightTagToHtml(text: string) {
|
||||||
|
return text?.replace(/\{\{(.*?)\}\}/g, '<span class="bg-primary-500/20 inline-block">{{ $1 }}</span>').trim()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-motion
|
||||||
|
flex="~ row" items-center gap-2
|
||||||
|
:initial="{ opacity: 0, x: 10 }"
|
||||||
|
:enter="{ opacity: 1, x: 0 }"
|
||||||
|
:leave="{ opacity: 0, x: -10 }"
|
||||||
|
:duration="250"
|
||||||
|
>
|
||||||
|
<button @click="router.back()">
|
||||||
|
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||||
|
</button>
|
||||||
|
<h1 relative>
|
||||||
|
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||||
|
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
|
||||||
|
</div>
|
||||||
|
<div text-nowrap text-3xl font-semibold>
|
||||||
|
{{ t('settings.pages.card.title') }}
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4">
|
||||||
|
<div
|
||||||
|
v-if="card"
|
||||||
|
bg="neutral-50 dark:[rgba(0,0,0,0.3)]"
|
||||||
|
rounded-xl p-5 flex="~ col gap-5"
|
||||||
|
border="~ neutral-200/50 dark:neutral-700/30"
|
||||||
|
shadow="sm dark:md"
|
||||||
|
transition="all duration-300"
|
||||||
|
class="backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div flex="~ col" gap-3>
|
||||||
|
<div flex="~ row" items-center justify-between>
|
||||||
|
<div>
|
||||||
|
<h1 text-2xl font-bold class="from-primary-500 to-primary-400 bg-gradient-to-r bg-clip-text text-transparent">
|
||||||
|
{{ card.name }}
|
||||||
|
</h1>
|
||||||
|
<div mt-1 text-sm text-neutral-500 dark:text-neutral-400>
|
||||||
|
v{{ card.version }}
|
||||||
|
<template v-if="card.creator">
|
||||||
|
· {{ t('settings.pages.card.created_by') }} <span font-medium>{{ card.creator }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div flex="~ row" gap-2>
|
||||||
|
<!-- Delete button -->
|
||||||
|
<AlertDialogRoot v-if="id !== 'default'" v-model:open="showDeleteConfirm">
|
||||||
|
<AlertDialogTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
:label="t('settings.pages.card.delete')"
|
||||||
|
/>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/50" />
|
||||||
|
<AlertDialogContent
|
||||||
|
class="fixed left-1/2 top-1/2 z-50 max-w-md w-full border border-neutral-200 rounded-xl bg-white p-6 shadow-xl -translate-x-1/2 -translate-y-1/2 dark:border-neutral-700 dark:bg-neutral-800"
|
||||||
|
>
|
||||||
|
<AlertDialogTitle class="mb-4 text-xl font-bold">
|
||||||
|
{{ t('settings.pages.card.delete_card') }}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription class="mb-6">
|
||||||
|
{{ t('settings.pages.card.delete_confirmation') }} <b>"{{ card.name }}"</b>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end gap-3">
|
||||||
|
<AlertDialogCancel as-child>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
:label="t('settings.pages.card.cancel')"
|
||||||
|
@click="() => showDeleteConfirm = false"
|
||||||
|
/>
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction as-child>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
:label="t('settings.pages.card.delete')"
|
||||||
|
@click="handleDeleteConfirm"
|
||||||
|
/>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</div>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
</AlertDialogRoot>
|
||||||
|
|
||||||
|
<!-- Activation button -->
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
:label="isActive ? t('settings.pages.card.active') : t('settings.pages.card.activate')"
|
||||||
|
:disabled="isActive"
|
||||||
|
:class="{ 'animate-pulse': isActivating }"
|
||||||
|
@click="handleActivate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Creator notes -->
|
||||||
|
<Section v-if="card.notes" :title="t('settings.pages.card.creator_notes')" icon="i-solar:notes-linear">
|
||||||
|
<div
|
||||||
|
bg="white/60 dark:black/30"
|
||||||
|
border="~ neutral-200/50 dark:neutral-700/30"
|
||||||
|
max-h-80 overflow-auto whitespace-pre-line rounded-lg p-4 text-neutral-700 dark:text-neutral-300 transition="all duration-200"
|
||||||
|
hover="bg-white/80 dark:bg-black/40"
|
||||||
|
v-html="hightlightTagToHtml(card.notes)"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<!-- Description section -->
|
||||||
|
<Section v-if="card.description" :title="t('settings.pages.card.description_label')" icon="i-solar:document-text-linear">
|
||||||
|
<div
|
||||||
|
bg="white/60 dark:black/30"
|
||||||
|
max-h-80 overflow-auto whitespace-pre-line rounded-lg p-4
|
||||||
|
text="neutral-600 dark:neutral-300"
|
||||||
|
border="~ neutral-200/50 dark:neutral-700/30"
|
||||||
|
v-html="hightlightTagToHtml(card.description)"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<!-- Character -->
|
||||||
|
<template v-if="Object.values(characterSettings).some(value => !!value)">
|
||||||
|
<Section :title="t('settings.pages.card.character')" icon="i-solar:user-rounded-linear">
|
||||||
|
<div flex="~ col" gap-4>
|
||||||
|
<template v-for="(value, key) in characterSettings" :key="key">
|
||||||
|
<div v-if="value" flex="~ col" gap-2>
|
||||||
|
<h2 text-lg text-neutral-500 font-medium dark:text-neutral-400>
|
||||||
|
{{ t(`settings.pages.card.${key.toLowerCase()}`) }}
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
bg="white/60 dark:black/30"
|
||||||
|
border="~ neutral-200/50 dark:neutral-700/30"
|
||||||
|
transition="all duration-200"
|
||||||
|
hover="bg-white/80 dark:bg-black/40"
|
||||||
|
max-h-80 overflow-auto whitespace-pre-line rounded-lg p-3 text-neutral-700 dark:text-neutral-300
|
||||||
|
v-html="hightlightTagToHtml(value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Modules -->
|
||||||
|
<Section :title="t('settings.pages.card.modules')" icon="i-solar:tuning-square-linear">
|
||||||
|
<div grid="~ cols-1 sm:cols-3" gap-4>
|
||||||
|
<div
|
||||||
|
flex="~ col"
|
||||||
|
bg="white/60 dark:black/30"
|
||||||
|
gap-1 rounded-lg p-3
|
||||||
|
border="~ neutral-200/50 dark:neutral-700/30"
|
||||||
|
transition="all duration-200"
|
||||||
|
hover="bg-white/80 dark:bg-black/40"
|
||||||
|
>
|
||||||
|
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
||||||
|
<div i-lucide:ghost />
|
||||||
|
{{ t('settings.pages.card.consciousness.model') }}
|
||||||
|
</span>
|
||||||
|
<div truncate font-medium>
|
||||||
|
{{ moduleSettings.consciousness ?? 'default' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
flex="~ col"
|
||||||
|
bg="white/60 dark:black/30"
|
||||||
|
gap-2 rounded-lg p-3
|
||||||
|
border="~ neutral-200/50 dark:neutral-700/30"
|
||||||
|
transition="all duration-200"
|
||||||
|
hover="bg-white/80 dark:bg-black/40"
|
||||||
|
>
|
||||||
|
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
||||||
|
<div i-lucide:mic />
|
||||||
|
{{ t('settings.pages.card.speech.model') }}
|
||||||
|
</span>
|
||||||
|
<div truncate font-medium>
|
||||||
|
{{ moduleSettings.speech ?? 'default' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
flex="~ col"
|
||||||
|
bg="white/60 dark:black/30"
|
||||||
|
gap-2 rounded-lg p-3
|
||||||
|
border="~ neutral-200/50 dark:neutral-700/30"
|
||||||
|
transition="all duration-200"
|
||||||
|
hover="bg-white/80 dark:bg-black/40"
|
||||||
|
>
|
||||||
|
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
||||||
|
<div i-lucide:music />
|
||||||
|
{{ t('settings.pages.card.speech.voice') }}
|
||||||
|
</span>
|
||||||
|
<div truncate font-medium>
|
||||||
|
{{ moduleSettings.voice ?? 'default' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
bg="neutral-50/50 dark:neutral-900/50"
|
||||||
|
rounded-xl p-8 text-center
|
||||||
|
border="~ neutral-200/50 dark:neutral-700/30"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<div i-solar:card-search-broken mx-auto mb-3 text-6xl text-neutral-400 />
|
||||||
|
{{ t('settings.pages.card.card_not_found') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||||
|
<div text="40" i-lucide:id-card />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { AiriCard } from '@proj-airi/stage-ui/stores'
|
|
||||||
|
|
||||||
import { Button, Section } from '@proj-airi/stage-ui/components'
|
|
||||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import {
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogRoot,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from 'radix-vue'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cardId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: 'activate'): void
|
|
||||||
(e: 'delete'): void
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const cardStore = useAiriCardStore()
|
|
||||||
const { getCard, removeCard } = cardStore
|
|
||||||
const { activeCardId } = storeToRefs(cardStore)
|
|
||||||
|
|
||||||
// Get current card
|
|
||||||
const card = computed<AiriCard | undefined>(() => getCard(props.cardId))
|
|
||||||
|
|
||||||
// Get module settings
|
|
||||||
const moduleSettings = computed(() => {
|
|
||||||
const airiExt = card.value?.extensions?.airi?.modules
|
|
||||||
return {
|
|
||||||
consciousness: airiExt?.consciousness?.model || '',
|
|
||||||
speech: airiExt?.speech?.model || '',
|
|
||||||
voice: airiExt?.speech?.voice_id || '',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get character settings
|
|
||||||
const characterSettings = computed(() => {
|
|
||||||
if (!card.value)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
personality: card.value.personality,
|
|
||||||
scenario: card.value.scenario,
|
|
||||||
systemPrompt: card.value.systemPrompt,
|
|
||||||
postHistoryInstructions: card.value.postHistoryInstructions,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if card is active
|
|
||||||
const isActive = computed(() => props.cardId === activeCardId.value)
|
|
||||||
|
|
||||||
// Activate card
|
|
||||||
function activateCard() {
|
|
||||||
activeCardId.value = props.cardId
|
|
||||||
emit('activate')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation control for card activation
|
|
||||||
const isActivating = ref(false)
|
|
||||||
|
|
||||||
function handleActivate() {
|
|
||||||
isActivating.value = true
|
|
||||||
setTimeout(() => {
|
|
||||||
activateCard()
|
|
||||||
isActivating.value = false
|
|
||||||
activeCardId.value = 'default'
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete card confirmation
|
|
||||||
const showDeleteConfirm = ref(false)
|
|
||||||
|
|
||||||
function handleDeleteConfirm() {
|
|
||||||
if (card.value) {
|
|
||||||
removeCard(props.cardId)
|
|
||||||
emit('delete')
|
|
||||||
activateCard()
|
|
||||||
}
|
|
||||||
showDeleteConfirm.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function hightlightTagToHtml(text: string) {
|
|
||||||
return text?.replace(/\{\{(.*?)\}\}/g, '<span class="bg-primary-500/20 inline-block">{{ $1 }}</span>').trim()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="card"
|
|
||||||
bg="neutral-50 dark:[rgba(0,0,0,0.3)]"
|
|
||||||
rounded-xl p-5 flex="~ col gap-5"
|
|
||||||
border="~ neutral-200/50 dark:neutral-700/30"
|
|
||||||
shadow="sm dark:md"
|
|
||||||
transition="all duration-300"
|
|
||||||
class="backdrop-blur-sm"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
|
||||||
<div flex="~ col" gap-3>
|
|
||||||
<div flex="~ row" items-center justify-between>
|
|
||||||
<div>
|
|
||||||
<h1 text-2xl font-bold class="from-primary-500 to-primary-400 bg-gradient-to-r bg-clip-text text-transparent">
|
|
||||||
{{ card.name }}
|
|
||||||
</h1>
|
|
||||||
<div mt-1 text-sm text-neutral-500 dark:text-neutral-400>
|
|
||||||
v{{ card.version }}
|
|
||||||
<template v-if="card.creator">
|
|
||||||
· {{ t('settings.pages.card.created_by') }} <span font-medium>{{ card.creator }}</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action buttons -->
|
|
||||||
<div flex="~ row" gap-2>
|
|
||||||
<!-- Delete button -->
|
|
||||||
<AlertDialogRoot v-if="props.cardId !== 'default'" v-model:open="showDeleteConfirm">
|
|
||||||
<AlertDialogTrigger as-child>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
:label="t('settings.pages.card.delete')"
|
|
||||||
/>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/50" />
|
|
||||||
<AlertDialogContent
|
|
||||||
class="fixed left-1/2 top-1/2 z-50 max-w-md w-full border border-neutral-200 rounded-xl bg-white p-6 shadow-xl -translate-x-1/2 -translate-y-1/2 dark:border-neutral-700 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<AlertDialogTitle class="mb-4 text-xl font-bold">
|
|
||||||
{{ t('settings.pages.card.delete_card') }}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription class="mb-6">
|
|
||||||
{{ t('settings.pages.card.delete_confirmation') }} <b>"{{ card.name }}"</b>
|
|
||||||
</AlertDialogDescription>
|
|
||||||
|
|
||||||
<div class="flex flex-row justify-end gap-3">
|
|
||||||
<AlertDialogCancel as-child>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
:label="t('settings.pages.card.cancel')"
|
|
||||||
@click="() => showDeleteConfirm = false"
|
|
||||||
/>
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction as-child>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
:label="t('settings.pages.card.delete')"
|
|
||||||
@click="handleDeleteConfirm"
|
|
||||||
/>
|
|
||||||
</AlertDialogAction>
|
|
||||||
</div>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialogPortal>
|
|
||||||
</AlertDialogRoot>
|
|
||||||
|
|
||||||
<!-- Activation button -->
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
:label="isActive ? t('settings.pages.card.active') : t('settings.pages.card.activate')"
|
|
||||||
:disabled="isActive"
|
|
||||||
:class="{ 'animate-pulse': isActivating }"
|
|
||||||
@click="handleActivate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creator notes -->
|
|
||||||
<Section v-if="card.notes" :title="t('settings.pages.card.creator_notes')" icon="i-solar:notes-bold-duotone">
|
|
||||||
<div
|
|
||||||
bg="white/60 dark:black/30"
|
|
||||||
whitespace-pre-line rounded-lg p-4
|
|
||||||
text-neutral-700 dark:text-neutral-300
|
|
||||||
border="~ neutral-200/50 dark:neutral-700/30"
|
|
||||||
transition="all duration-200"
|
|
||||||
hover="bg-white/80 dark:bg-black/40"
|
|
||||||
v-html="hightlightTagToHtml(card.notes)"
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<!-- Description section -->
|
|
||||||
<Section v-if="card.description" :title="t('settings.pages.card.description_label')" icon="i-solar:document-text-bold-duotone">
|
|
||||||
<div
|
|
||||||
bg="white/60 dark:black/30"
|
|
||||||
whitespace-pre-line
|
|
||||||
rounded-lg
|
|
||||||
p-4
|
|
||||||
text="neutral-600 dark:neutral-300"
|
|
||||||
border="~ neutral-200/50 dark:neutral-700/30"
|
|
||||||
v-html="hightlightTagToHtml(card.description)"
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<!-- Character -->
|
|
||||||
<template v-if="Object.values(characterSettings).some(value => !!value)">
|
|
||||||
<Section :title="t('settings.pages.card.character')" icon="i-solar:user-rounded-bold-duotone">
|
|
||||||
<div flex="~ col" gap-4>
|
|
||||||
<template v-for="(value, key) in characterSettings" :key="key">
|
|
||||||
<div v-if="value" flex="~ col" gap-2>
|
|
||||||
<h2 text-lg text-neutral-500 font-medium dark:text-neutral-400>
|
|
||||||
{{ t(`settings.pages.card.${key.toLowerCase()}`) }}
|
|
||||||
</h2>
|
|
||||||
<div
|
|
||||||
bg="white/60 dark:black/30"
|
|
||||||
border="~ neutral-200/50 dark:neutral-700/30"
|
|
||||||
transition="all duration-200"
|
|
||||||
hover="bg-white/80 dark:bg-black/40"
|
|
||||||
max-h-60 overflow-auto whitespace-pre-line rounded-lg p-3 text-neutral-700 dark:text-neutral-300
|
|
||||||
v-html="hightlightTagToHtml(value)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Modules -->
|
|
||||||
<Section :title="t('settings.pages.card.modules')" icon="i-solar:tuning-square-bold-duotone">
|
|
||||||
<div grid="~ cols-1 sm:cols-3" gap-4>
|
|
||||||
<div
|
|
||||||
flex="~ col"
|
|
||||||
bg="white/60 dark:black/30"
|
|
||||||
gap-1 rounded-lg p-3
|
|
||||||
border="~ neutral-200/50 dark:neutral-700/30"
|
|
||||||
transition="all duration-200"
|
|
||||||
hover="bg-white/80 dark:bg-black/40"
|
|
||||||
>
|
|
||||||
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
|
||||||
<div i-lucide:ghost />
|
|
||||||
{{ t('settings.pages.modules.consciousness.title') }}
|
|
||||||
</span>
|
|
||||||
<div truncate font-medium>
|
|
||||||
{{ moduleSettings.consciousness }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
flex="~ col"
|
|
||||||
bg="white/60 dark:black/30"
|
|
||||||
gap-2 rounded-lg p-3
|
|
||||||
border="~ neutral-200/50 dark:neutral-700/30"
|
|
||||||
transition="all duration-200"
|
|
||||||
hover="bg-white/80 dark:bg-black/40"
|
|
||||||
>
|
|
||||||
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
|
||||||
<div i-lucide:mic />
|
|
||||||
{{ t('settings.pages.modules.speech.title') }}
|
|
||||||
</span>
|
|
||||||
<div truncate font-medium>
|
|
||||||
{{ moduleSettings.speech }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
flex="~ col"
|
|
||||||
bg="white/60 dark:black/30"
|
|
||||||
gap-2 rounded-lg p-3
|
|
||||||
border="~ neutral-200/50 dark:neutral-700/30"
|
|
||||||
transition="all duration-200"
|
|
||||||
hover="bg-white/80 dark:bg-black/40"
|
|
||||||
>
|
|
||||||
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
|
||||||
<div i-solar:music-notes-bold-duotone />
|
|
||||||
{{ t('settings.pages.card.voice_id') }}
|
|
||||||
</span>
|
|
||||||
<div truncate font-medium>
|
|
||||||
{{ moduleSettings.voice }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
bg="neutral-50/50 dark:neutral-900/50"
|
|
||||||
rounded-xl p-8 text-center
|
|
||||||
border="~ neutral-200/50 dark:neutral-700/30"
|
|
||||||
shadow="sm"
|
|
||||||
>
|
|
||||||
<div i-solar:card-search-broken mx-auto mb-3 text-6xl text-neutral-400 />
|
|
||||||
{{ t('settings.pages.card.card_not_found') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -5,12 +5,10 @@ import { Button } from '@proj-airi/stage-ui/components/Button'
|
|||||||
import { RadioCardDetailManySelect } from '@proj-airi/stage-ui/components/Form'
|
import { RadioCardDetailManySelect } from '@proj-airi/stage-ui/components/Form'
|
||||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores'
|
import { useAiriCardStore } from '@proj-airi/stage-ui/stores'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import AiriCardView from './components/AiriCardView.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const cardStore = useAiriCardStore()
|
const cardStore = useAiriCardStore()
|
||||||
@@ -66,21 +64,10 @@ async function handleUpload() {
|
|||||||
fileInput.click()
|
fileInput.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
watch(selectedCardId, (cardId) => {
|
||||||
* Sets the selected card as active
|
if (cardId)
|
||||||
*/
|
router.push(`/settings/airi-card/${cardId}`)
|
||||||
function activateSelectedCard() {
|
})
|
||||||
if (selectedCardId.value)
|
|
||||||
activeCardId.value = selectedCardId.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles card deletion
|
|
||||||
*/
|
|
||||||
function handleCardDelete() {
|
|
||||||
// Reset selected card ID if it's the one being deleted
|
|
||||||
selectedCardId.value = cardsArray.value.length > 0 ? cardsArray.value[0].id : ''
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -129,20 +116,11 @@ function handleCardDelete() {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Card content area -->
|
<!-- Background decoration -->
|
||||||
<div v-if="selectedCardId && cards.size > 0" mt-6>
|
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||||
<AiriCardView
|
<div text="40" i-lucide:id-card />
|
||||||
:card-id="selectedCardId"
|
|
||||||
@activate="activateSelectedCard"
|
|
||||||
@delete="handleCardDelete"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Background decoration -->
|
|
||||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
|
||||||
<div text="40" i-lucide:id-card />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Section } from '@proj-airi/stage-ui/components'
|
||||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from '@vueuse/core'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
@@ -6,7 +7,6 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import CheckBar from '../../../components/Settings/CheckBar.vue'
|
import CheckBar from '../../../components/Settings/CheckBar.vue'
|
||||||
import ColorPalette from '../../../components/Settings/ColorPalette.vue'
|
import ColorPalette from '../../../components/Settings/ColorPalette.vue'
|
||||||
import Section from '../../../components/Settings/Section.vue'
|
|
||||||
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||||
import COLOR_PRESETS from './color-presets.json'
|
import COLOR_PRESETS from './color-presets.json'
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions],
|
|||||||
/>
|
/>
|
||||||
<!-- Language Setting -->
|
<!-- Language Setting -->
|
||||||
<div
|
<div
|
||||||
class="setting-bar text-sm"
|
class="w-full flex items-center justify-between rounded-lg px-4 py-3 text-sm outline-none transition-all duration-250 ease-in-out"
|
||||||
bg="neutral-50 dark:neutral-800"
|
bg="neutral-50 dark:neutral-800"
|
||||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||||
>
|
>
|
||||||
@@ -129,7 +129,7 @@ watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions],
|
|||||||
<Section title="settings.pages.themes.sections.section.theme-presets.title" icon="i-solar:magic-stick-2-bold-duotone">
|
<Section title="settings.pages.themes.sections.section.theme-presets.title" icon="i-solar:magic-stick-2-bold-duotone">
|
||||||
<div
|
<div
|
||||||
v-for="({ title, description, colors }, i) in $tm('settings.pages.themes.sections.section.theme-presets.presets')" :key="i"
|
v-for="({ title, description, colors }, i) in $tm('settings.pages.themes.sections.section.theme-presets.presets')" :key="i"
|
||||||
class="setting-bar"
|
class="w-full flex items-center justify-between rounded-lg px-4 py-3 outline-none transition-all duration-250 ease-in-out"
|
||||||
bg="neutral-100 dark:neutral-800"
|
bg="neutral-100 dark:neutral-800"
|
||||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const settings = computed(() => [
|
|||||||
{
|
{
|
||||||
title: t('settings.pages.card.title'),
|
title: t('settings.pages.card.title'),
|
||||||
description: t('settings.pages.card.description'),
|
description: t('settings.pages.card.description'),
|
||||||
icon: 'i-lucide:card',
|
icon: 'i-lucide:id-card',
|
||||||
to: '/settings/airi-card',
|
to: '/settings/airi-card',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,8 +34,3 @@ html.dark {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-bar {
|
|
||||||
--at-apply: transition-all ease-in-out duration-250 w-full flex items-center justify-between rounded-lg px-4 py-3
|
|
||||||
outline-none;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
const messages = ref<Array<Message | ErrorMessage>>([
|
const messages = ref<Array<Message | ErrorMessage>>([
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: systemPrompt.value.replace(/\{\{user\}\}/g, 'user'),
|
content: systemPrompt.value, // TODO: compose, replace {{ user }} tag, etc
|
||||||
} satisfies SystemMessage,
|
} satisfies SystemMessage,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user