chore: improved UI

This commit is contained in:
Neko Ayaka
2024-12-07 02:42:07 +08:00
parent 8f9a0e76cd
commit e2f1f7bd37
9 changed files with 205 additions and 133 deletions
+1
View File
@@ -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",
+75 -103
View File
@@ -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>
+103 -18
View File
@@ -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>
+6 -6
View File
@@ -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
+2
View File
@@ -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,
}
})
+10
View File
@@ -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':
+1
View File
@@ -26,6 +26,7 @@ export default defineConfig({
sans: 'DM Sans',
serif: 'DM Serif Display',
mono: 'DM Mono',
cute: 'Kiwi Maru',
},
}),
],