chore: improved UI
This commit is contained in:
@@ -64,6 +64,7 @@
|
||||
"@airi-proj/elevenlabs": "workspace:^",
|
||||
"@iconify-json/carbon": "^1.2.4",
|
||||
"@iconify-json/eos-icons": "^1.2.1",
|
||||
"@iconify-json/solar": "^1.2.1",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.0",
|
||||
"@shikijs/markdown-it": "^1.24.0",
|
||||
"@types/markdown-it-link-attributes": "^3.0.5",
|
||||
|
||||
@@ -41,7 +41,7 @@ const listening = ref(false)
|
||||
const live2DViewerRef = ref<{ setMotion: (motionName: string) => Promise<void> }>()
|
||||
const supportedModels = ref<{ id: string, name?: string }[]>([])
|
||||
const messageInput = ref<string>('')
|
||||
const messages = ref<Array<Message>>([SystemPromptV2 as SystemMessage])
|
||||
const messages = ref<Array<Message>>([SystemPromptV2 as SystemMessage, { role: 'assistant', content: 'A'.repeat(1000) }, { role: 'assistant', content: 'A'.repeat(1000) }])
|
||||
const streamingMessage = ref<AssistantMessage>({ role: 'assistant', content: '' })
|
||||
const audioAnalyser = ref<AnalyserNode>()
|
||||
const mouthOpenSize = ref(0)
|
||||
@@ -269,52 +269,17 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-full max-h="[100vh]" p="2" flex="~ col">
|
||||
<div flex="~" gap-2>
|
||||
<fieldset
|
||||
flex="~ row"
|
||||
bg="zinc-100 dark:zinc-700"
|
||||
text="sm zinc-400 dark:zinc-500"
|
||||
appearance-none gap-1 rounded-lg rounded-md border-none p-1
|
||||
>
|
||||
<label
|
||||
h-fit cursor-pointer
|
||||
:class="[stageView === '2d' ? 'bg-zinc-300 text-zinc-900 dark:bg-zinc-200 dark:text-zinc-800' : '']"
|
||||
rounded-md px-4 py-2
|
||||
>
|
||||
<input
|
||||
v-model="stageView"
|
||||
:checked="stageView === '2d'"
|
||||
:aria-checked="stageView === '2d'"
|
||||
name="stageView"
|
||||
type="radio"
|
||||
role="radio"
|
||||
value="2d"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div select-none>2D</div>
|
||||
</label>
|
||||
<label
|
||||
h-fit cursor-pointer
|
||||
:class="[stageView === '3d' ? 'bg-zinc-300 text-zinc-900 dark:bg-zinc-200 dark:text-zinc-800' : '']"
|
||||
rounded-md px-4 py-2
|
||||
>
|
||||
<input
|
||||
v-model="stageView"
|
||||
:checked="stageView === '3d'"
|
||||
:aria-checked="stageView === '3d'"
|
||||
name="stageView"
|
||||
type="radio"
|
||||
role="radio"
|
||||
value="3d"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div select-none>3D</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div h-full max-h="[100vh]" max-w="[100vw]" p="2" flex="~ col" overflow-hidden>
|
||||
<div flex="~" mb-1 w-full gap-2>
|
||||
<div flex="~ 1" w-full items-center gap-2 text-nowrap text-2xl>
|
||||
<div i-solar:cat-outline text="[#ed869d]" />
|
||||
<div font-cute>
|
||||
<span>アイリ</span>
|
||||
</div>
|
||||
</div>
|
||||
<Settings />
|
||||
</div>
|
||||
<div flex="~ row 1" max-h="[calc(100vh-210px)]" relative h-full w-full items-end gap-2>
|
||||
<div flex="~ row 1" max-h="[calc(100vh-220px)]" relative h-full w-full items-end gap-2>
|
||||
<Live2DViewer
|
||||
v-if="stageView === '2d'"
|
||||
ref="live2DViewerRef"
|
||||
@@ -331,61 +296,62 @@ onUnmounted(() => {
|
||||
/>
|
||||
<div
|
||||
class="relative <lg:(absolute bottom-0 from-zinc-800/80 to-zinc-800/0 bg-gradient-to-t p-2)"
|
||||
px="<sm:2" py="<sm:2" rounded="<sm:lg"
|
||||
w="50% <lg:full" flex="~ col 1" gap-2 max-h="[calc(100vh-210px)]"
|
||||
px="<sm:2" py="<sm:2" rounded="lg"
|
||||
w="50% <lg:full" flex="~ col 1" overflow-hidden max-h="[calc(100vh-220px)]"
|
||||
>
|
||||
<div v-for="(message, index) in messages" :key="index">
|
||||
<div v-if="message.role === 'assistant'" flex mr="12">
|
||||
<div
|
||||
mr-2 h-10
|
||||
min-h-10 min-w-10 w-10
|
||||
overflow-hidden rounded-full
|
||||
border="solid 3"
|
||||
transition="all ease-in-out" duration-100
|
||||
:style="{
|
||||
borderColor: `rgba(236, 72, 153, ${nowSpeakingAvatarBorderOpacity.toFixed(2)})`,
|
||||
}"
|
||||
>
|
||||
<img :src="Avatar">
|
||||
</div>
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="pink-50 dark:pink-900"
|
||||
p="2"
|
||||
border="2 solid pink dark:pink-700"
|
||||
rounded-lg
|
||||
h="unset <sm:fit"
|
||||
>
|
||||
<div>
|
||||
<span font-semibold class="inline <sm:hidden">Neuro</span>
|
||||
<div h-full w-full overflow-scroll>
|
||||
<div v-for="(message, index) in messages" :key="index" mb-2>
|
||||
<div v-if="message.role === 'assistant'" flex mr="12">
|
||||
<div
|
||||
mr-2 h-10
|
||||
min-h-10 min-w-10 w-10
|
||||
overflow-hidden rounded-full
|
||||
border="solid 3"
|
||||
transition="all ease-in-out" duration-100
|
||||
:style="{
|
||||
borderColor: `rgba(236, 72, 153, ${nowSpeakingAvatarBorderOpacity.toFixed(2)})`,
|
||||
}"
|
||||
>
|
||||
<img :src="Avatar">
|
||||
</div>
|
||||
<div v-if="message.content" text="base <sm:xs" v-html="process(message.content as string)" />
|
||||
<div v-else i-eos-icons:three-dots-loading />
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="pink-50 dark:pink-900"
|
||||
border="2 solid pink dark:pink-700"
|
||||
rounded-lg px-2 py-1
|
||||
h="unset <sm:fit"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="white/50" font-semibold class="inline <sm:hidden">Airi</span>
|
||||
</div>
|
||||
<div v-if="message.content" class="markdown-content" text="base <sm:xs" v-html="process(message.content as string)" />
|
||||
<div v-else i-eos-icons:three-dots-loading />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.role === 'user'" flex="~ row-reverse" ml="12">
|
||||
<div
|
||||
class="block <sm:hidden"
|
||||
border="purple solid 3"
|
||||
ml="2"
|
||||
h-10 min-h-10 min-w-10 w-10
|
||||
overflow-hidden rounded-full
|
||||
>
|
||||
<div i-carbon:user-avatar-filled text="purple" h-full w-full p="0" m="0" />
|
||||
</div>
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="purple-50 dark:purple-900"
|
||||
p="2"
|
||||
border="2 solid purple dark:purple-700"
|
||||
rounded-lg
|
||||
h="unset <sm:fit"
|
||||
>
|
||||
<div>
|
||||
<span font-semibold class="inline <sm:hidden">You</span>
|
||||
<div v-else-if="message.role === 'user'" flex="~ row-reverse" ml="12">
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="pink-50 dark:pink-900"
|
||||
border="2 solid pink dark:pink-700"
|
||||
rounded-lg px-2 py-1
|
||||
h="unset <sm:fit"
|
||||
>
|
||||
<div i-carbon:user-avatar-filled text="purple" h-full w-full p="0" m="0" />
|
||||
</div>
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="purple-50 dark:purple-900"
|
||||
px="2"
|
||||
border="2 solid purple dark:purple-700"
|
||||
rounded-lg
|
||||
h="unset <sm:fit"
|
||||
>
|
||||
<div>
|
||||
<span text-xs text="white/50" font-semibold class="inline <sm:hidden">You</span>
|
||||
</div>
|
||||
<div v-if="message.content" class="markdown-content" text="base <sm:xs" whitespace-nowrap v-html="process(message.content as string)" />
|
||||
<div v-else />
|
||||
</div>
|
||||
<div v-if="message.content" text="base <sm:xs" v-html="process(message.content as string)" />
|
||||
<div v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,24 +380,21 @@ onUnmounted(() => {
|
||||
v-model="messageInput"
|
||||
placeholder="Message"
|
||||
p="2" bg="zinc-100 dark:zinc-700"
|
||||
w="[95%]" rounded-lg outline-none min-h="[100px]"
|
||||
w="[92%]" rounded-lg outline-none min-h="[100px]"
|
||||
@submit="onSendMessage"
|
||||
/>
|
||||
<button
|
||||
flex="~ row"
|
||||
p="2" bg="zinc-100 dark:zinc-700"
|
||||
w="[5%]" items-center rounded-lg outline-none
|
||||
w="[8%]" items-center justify-center rounded-lg outline-none
|
||||
transition="all ease-in-out"
|
||||
@click="listening = !listening"
|
||||
>
|
||||
<Transition mode="out-in">
|
||||
<div v-if="listening" flex="~ row" items-center space-x-1>
|
||||
<div v-if="listening" flex="~ row" items-center justify-center space-x-1>
|
||||
<div i-carbon:microphone-filled text-red />
|
||||
<span>
|
||||
Listening...
|
||||
</span>
|
||||
</div>
|
||||
<div v-else flex="~ row" items-center space-x-1>
|
||||
<div v-else flex="~ row" items-center justify-center space-x-1>
|
||||
<div i-carbon:microphone text-inherit />
|
||||
<span>
|
||||
Talk
|
||||
@@ -455,4 +418,13 @@ onUnmounted(() => {
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
word-wrap: break-word;
|
||||
word-break: normal;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: auto;
|
||||
white-space: break-spaces;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,30 +2,115 @@
|
||||
import { useSettings } from '../stores/settings'
|
||||
|
||||
const settings = useSettings()
|
||||
const show = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div space-x="2" flex="~ row" w-full>
|
||||
<div flex="~ row" w-full>
|
||||
<input
|
||||
v-model="settings.openAiApiBaseURL"
|
||||
placeholder="Input your API base URL"
|
||||
p="2" bg="zinc-100 dark:zinc-700" w-full rounded-lg outline-none
|
||||
<div flex="~" gap-2>
|
||||
<fieldset
|
||||
flex="~ row"
|
||||
bg="zinc-100 dark:zinc-700"
|
||||
text="sm zinc-400 dark:zinc-500"
|
||||
appearance-none gap-1 rounded-lg rounded-md border-none p-1
|
||||
>
|
||||
<label
|
||||
h-fit cursor-pointer
|
||||
:class="[settings.stageView === '2d' ? 'bg-zinc-300 text-zinc-900 dark:bg-zinc-200 dark:text-zinc-800' : '']"
|
||||
rounded-md px-4 py-2
|
||||
>
|
||||
</div>
|
||||
<div flex="~ row" w-full>
|
||||
<input
|
||||
v-model="settings.openAiApiKey"
|
||||
placeholder="Input your API key"
|
||||
p="2" bg="zinc-100 dark:zinc-700" w-full rounded-lg outline-none
|
||||
<input
|
||||
v-model="settings.stageView"
|
||||
:checked="settings.stageView === '2d'"
|
||||
:aria-checked="settings.stageView === '2d'"
|
||||
name="settings.stageView"
|
||||
type="radio"
|
||||
role="radio"
|
||||
value="2d"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div select-none>2D</div>
|
||||
</label>
|
||||
<label
|
||||
h-fit cursor-pointer
|
||||
:class="[settings.stageView === '3d' ? 'bg-zinc-300 text-zinc-900 dark:bg-zinc-200 dark:text-zinc-800' : '']"
|
||||
rounded-md px-4 py-2
|
||||
>
|
||||
</div>
|
||||
<div flex="~ row" w-full>
|
||||
<input
|
||||
v-model="settings.elevenLabsApiKey"
|
||||
placeholder="Input your ElevenLabs API key"
|
||||
p="2" bg="zinc-100 dark:zinc-700" w-full rounded-lg outline-none
|
||||
<input
|
||||
v-model="settings.stageView"
|
||||
:checked="settings.stageView === '3d'"
|
||||
:aria-checked="settings.stageView === '3d'"
|
||||
name="stageView"
|
||||
type="radio"
|
||||
role="radio"
|
||||
value="3d"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div select-none>3D</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div relative>
|
||||
<div
|
||||
flex="~ row"
|
||||
bg="zinc-100 dark:zinc-700"
|
||||
text="sm zinc-400 dark:zinc-500"
|
||||
h-fit w-fit appearance-none gap-1 rounded-lg rounded-md border-none p-1
|
||||
>
|
||||
<label
|
||||
h-fit cursor-pointer
|
||||
:class="[show ? 'bg-zinc-300 text-zinc-900 dark:bg-zinc-200 dark:text-zinc-800' : '']"
|
||||
rounded-md px-2 py-2
|
||||
>
|
||||
<input
|
||||
v-model="show"
|
||||
:checked="show"
|
||||
:aria-checked="show"
|
||||
name="stageView"
|
||||
type="checkbox"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div select-none>
|
||||
<div i-carbon:settings text="text-zinc-900 dark:text-zinc-800 lg" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="show" w="120" right="0" bg="zinc-100 dark:zinc-700"
|
||||
grid="~ cols-[140px_1fr]" absolute z-10 my-2 items-center gap-1.5 rounded-lg p-2
|
||||
>
|
||||
<div text-sm>
|
||||
<span>OpenAI API BaseURL</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full text="sm">
|
||||
<input
|
||||
v-model="settings.openAiApiBaseURL"
|
||||
type="text"
|
||||
placeholder="Input your API base URL"
|
||||
bg="zinc-200 dark:zinc-800/50" w-full rounded-md px-2 py-1 font-mono outline-none
|
||||
>
|
||||
</div>
|
||||
<div text-sm>
|
||||
<span>OpenAI API Key</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full text="sm">
|
||||
<input
|
||||
v-model="settings.openAiApiKey"
|
||||
type="text"
|
||||
placeholder="Input your API key"
|
||||
bg="zinc-200 dark:zinc-800/50" w-full rounded-md px-2 py-1 font-mono outline-none
|
||||
>
|
||||
</div>
|
||||
<div text-sm>
|
||||
<span>ElevenLabs API Key</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full text="sm">
|
||||
<input
|
||||
v-model="settings.elevenLabsApiKey"
|
||||
type="text"
|
||||
placeholder="Input your ElevenLabs API key"
|
||||
bg="zinc-200 dark:zinc-800/50" w-full rounded-md px-2 py-1 font-mono outline-none
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -26,12 +26,12 @@ const vrmModelPositionZ = ref(-0.24)
|
||||
|
||||
<template>
|
||||
<Screen v-slot="{ canvasHeight, canvasWidth }" relative>
|
||||
<div z="10" top="2" absolute w-full gap-2 px-2 flex="~ col md:row">
|
||||
<div z="10" top="2" absolute w-full gap-2 flex="~ col md:row">
|
||||
<Collapsable h-fit w-full>
|
||||
<template #label>
|
||||
<span font-mono>Camera</span>
|
||||
</template>
|
||||
<div grid="~ cols-[20px_1fr_60px]" w-full gap-2 p-2 text-sm font-mono>
|
||||
<div grid="~ cols-[20px_1fr_60px]" w-full gap-1 p-2 text-sm font-mono>
|
||||
<div text="zinc-400 dark:zinc-500">
|
||||
<span>X</span>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@ const vrmModelPositionZ = ref(-0.24)
|
||||
<template #label>
|
||||
<span font-mono>Model</span>
|
||||
</template>
|
||||
<div grid="~ cols-[20px_1fr_60px]" w-full gap-2 p-2 text-sm font-mono>
|
||||
<div grid="~ cols-[20px_1fr_60px]" w-full gap-1 p-2 text-sm font-mono>
|
||||
<div text="zinc-400 dark:zinc-500">
|
||||
<span>X</span>
|
||||
</div>
|
||||
@@ -102,9 +102,10 @@ const vrmModelPositionZ = ref(-0.24)
|
||||
</Collapsable>
|
||||
</div>
|
||||
<TresCanvas :alpha="true" :antialias="true" :width="canvasWidth" :height="canvasHeight">
|
||||
<TresPerspectiveCamera :position="[cameraPositionX, cameraPositionY, cameraPositionZ]" />
|
||||
<TresDirectionalLight :color="0xFFFFFF" :intensity="0.6" :position="[1, 1, 1]" />
|
||||
<OrbitControls />
|
||||
<TresPerspectiveCamera :position="[cameraPositionX, cameraPositionY, cameraPositionZ]" />
|
||||
<TresDirectionalLight :color="0xFFFFFF" :intensity="1.2" :position="[1, 1, 1]" />
|
||||
<TresAmbientLight :color="0xFFFFFF" :intensity="1.5" />
|
||||
<VRMModel
|
||||
:model="props.model"
|
||||
:idle-animation="props.idleAnimation"
|
||||
@@ -112,7 +113,6 @@ const vrmModelPositionZ = ref(-0.24)
|
||||
@load-model-progress="(val) => emit('loadModelProgress', val)"
|
||||
@error="(val) => emit('error', val)"
|
||||
/>
|
||||
<TresAmbientLight :color="0xFFFFFF" :intensity="0.4" />
|
||||
</TresCanvas>
|
||||
</Screen>
|
||||
</template>
|
||||
|
||||
@@ -22,6 +22,12 @@ const vrmAnimationMixer = ref<AnimationMixer>()
|
||||
const { scene } = useTresContext()
|
||||
const { onBeforeRender } = useLoop()
|
||||
|
||||
watch(() => props.position, ([x, y, z]) => {
|
||||
if (vrm.value) {
|
||||
vrm.value.scene.position.set(x, y, z)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!scene.value) {
|
||||
return
|
||||
@@ -68,12 +74,6 @@ onUnmounted(() => {
|
||||
scene.value.remove(vrm.value.scene)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.position, ([x, y, z]) => {
|
||||
if (vrm.value) {
|
||||
vrm.value.scene.position.set(x, y, z)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -13,6 +13,7 @@ export function useVRMLoader() {
|
||||
|
||||
loader.crossOrigin = 'anonymous'
|
||||
loader.register(parser => new VRMLoaderPlugin(parser))
|
||||
// loader.register(parser => new VRMCoreLoaderPlugin(parser))
|
||||
loader.register(parser => new VRMAnimationLoaderPlugin(parser))
|
||||
|
||||
return loader
|
||||
|
||||
@@ -5,10 +5,12 @@ export const useSettings = defineStore('settings', () => {
|
||||
const openAiApiKey = useLocalStorage('settings/credentials/openai-api-key', '')
|
||||
const openAiApiBaseURL = useLocalStorage('settings/credentials/openai-api-base-url', '')
|
||||
const elevenLabsApiKey = useLocalStorage('settings/credentials/elevenlabs-api-key', '')
|
||||
const stageView = useLocalStorage('settings/stage/view/model-renderer', '2d')
|
||||
|
||||
return {
|
||||
openAiApiKey,
|
||||
openAiApiBaseURL,
|
||||
elevenLabsApiKey,
|
||||
stageView,
|
||||
}
|
||||
})
|
||||
|
||||
Generated
+10
@@ -235,6 +235,9 @@ importers:
|
||||
'@iconify-json/eos-icons':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
'@iconify-json/solar':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(@vue/compiler-dom@3.5.13)(eslint@9.16.0(jiti@2.4.0))(rollup@2.79.1)(typescript@5.7.2)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))
|
||||
@@ -1652,6 +1655,9 @@ packages:
|
||||
'@iconify-json/eos-icons@1.2.1':
|
||||
resolution: {integrity: sha512-nqucN9E670dRHDk5osNNObhxQg/eBI8wvYA+zWaOMsJR0g8VodF9z/cpDvMH7YyUm4elOF9r8cGIj3byFBFLRQ==}
|
||||
|
||||
'@iconify-json/solar@1.2.1':
|
||||
resolution: {integrity: sha512-LA3WZ7HOi5O/sEc2M+BcQNJYNFDnUKP+ZldmNf8kwndnCuFMo+fukt6tSu4BqYRsRgp/Ku4xnys1JGBDi+QtCQ==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
@@ -8112,6 +8118,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/solar@1.2.1':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@iconify/utils@2.1.33':
|
||||
|
||||
@@ -26,6 +26,7 @@ export default defineConfig({
|
||||
sans: 'DM Sans',
|
||||
serif: 'DM Serif Display',
|
||||
mono: 'DM Mono',
|
||||
cute: 'Kiwi Maru',
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user