feat(stage-*): dramatically improved mobile
This commit is contained in:
@@ -6,7 +6,7 @@ import WhisperWorker from '@proj-airi/stage-ui/libs/workers/worker?worker&url'
|
||||
import { toWAVBase64 } from '@proj-airi/audio'
|
||||
import { useMicVAD, useWhisper } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext, useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { BasicTextarea, TransitionVertical } from '@proj-airi/ui'
|
||||
import { BasicTextarea } from '@proj-airi/ui'
|
||||
import { useDevicesList } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
@@ -24,8 +24,8 @@ const providersStore = useProvidersStore()
|
||||
const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
const { themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
|
||||
const { audioInputs, ensurePermissions } = useDevicesList({ constraints: { audio: true } })
|
||||
const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { ensurePermissions } = useDevicesList({ constraints: { audio: true } })
|
||||
const { isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { send, onAfterSend, discoverToolsCompatibility } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { audioContext } = useAudioContext()
|
||||
@@ -104,17 +104,6 @@ async function handleTranscription(buffer: ArrayBufferLike) {
|
||||
generate({ type: 'generate', data: { audio: audioBase64, language: 'en' } })
|
||||
}
|
||||
|
||||
async function handleAudioInputChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const found = audioInputs.value.find(d => d.deviceId === target.value)
|
||||
if (!found) {
|
||||
selectedAudioDevice.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
selectedAudioDevice.value = found
|
||||
}
|
||||
|
||||
watch(isAudioInputOn, async (value) => {
|
||||
if (value === 'false') {
|
||||
destroy()
|
||||
@@ -195,46 +184,5 @@ onAfterSend(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div flex="~ row" gap-2 hidden>
|
||||
<div flex="~ row" relative text-white font-normal>
|
||||
<TransitionVertical>
|
||||
<fieldset
|
||||
v-if="showMicrophoneSelect"
|
||||
right="0" bottom="[calc(100%+8px)]" text="cyan-400 dark:white" bg="white dark:cyan-900"
|
||||
absolute z-30 h-fit rounded-2xl py-3 pl-3 pr-4 text-right text-nowrap text-sm font-sans
|
||||
>
|
||||
<label v-for="(input, index) in audioInputs" :key="index" class="[&_div_span]:dark:hover:bg-cyan-300 [&_div_span]:dark:hover:bg-cyan-900">
|
||||
<input type="radio" name="audioInput" :value="input.deviceId" hidden @change="handleAudioInputChange">
|
||||
<div flex="~ row" cursor-pointer items-center grid="cols-2">
|
||||
<div min-w="6">
|
||||
<div v-if="input.deviceId === selectedAudioDeviceId" i-solar:check-circle-line-duotone />
|
||||
</div>
|
||||
<span
|
||||
inline-block
|
||||
:class="[input.deviceId === selectedAudioDeviceId ? 'cyan-400 dark:text-white' : 'cyan-400/50 dark:text-white/50']"
|
||||
transition="all duration-250 ease-in-out"
|
||||
>
|
||||
{{ input.label }}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
</TransitionVertical>
|
||||
<label
|
||||
bg="complementary-100 hover:complementary-200 dark:complementary-800 dark:hover:complementary-700"
|
||||
transition="all duration-250 ease-in-out"
|
||||
:class="{ 'transition-colors-none': themeColorsHueDynamic }"
|
||||
text="complementary-400"
|
||||
h-fit flex cursor-pointer items-center justify-center gap-2 rounded-full px-4 py-2
|
||||
>
|
||||
<input v-model="showMicrophoneSelect" type="checkbox" hidden>
|
||||
<div i-solar:microphone-2-bold-duotone />
|
||||
<div>
|
||||
<span v-if="!listening">{{ t('settings.microphone') }}</span>
|
||||
<span v-else>Listening...</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import { Button } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'reset'): void
|
||||
}>()
|
||||
|
||||
const { stageView, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
|
||||
const mode = defineModel<'x' | 'y' | 'z' | 'scale'>({ required: true })
|
||||
|
||||
function handleViewControlsToggle(targetMode: 'x' | 'y' | 'z' | 'scale') {
|
||||
if (mode.value === targetMode) {
|
||||
emits('reset')
|
||||
return
|
||||
}
|
||||
|
||||
mode.value = targetMode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div w-full flex flex-1 items-center self-end justify-end gap-2>
|
||||
<Transition name="fade">
|
||||
<div v-if="stageViewControlsEnabled" w-full flex justify-between gap-2>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'x'" w-full @click="handleViewControlsToggle('x')">
|
||||
X
|
||||
</Button>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'y'" w-full @click="handleViewControlsToggle('y')">
|
||||
Y
|
||||
</Button>
|
||||
<Button v-if="stageView === '3d'" variant="secondary-muted" :toggled="mode === 'z'" w-full @click="handleViewControlsToggle('z')">
|
||||
Z
|
||||
</Button>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'scale'" w-full @click="handleViewControlsToggle('scale')">
|
||||
Scale
|
||||
</Button>
|
||||
</div>
|
||||
</Transition>
|
||||
<button
|
||||
w-fit flex items-center self-end justify-center justify-self-end rounded-xl p-2 backdrop-blur-md
|
||||
border="2 solid neutral-100/60 dark:neutral-800/30"
|
||||
bg="neutral-50/70 dark:neutral-800/70"
|
||||
title="View"
|
||||
text="neutral-500 dark:neutral-400"
|
||||
@click="stageViewControlsEnabled = !stageViewControlsEnabled"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="!stageViewControlsEnabled" i-solar:tuning-outline size-5 />
|
||||
<div v-else i-solar:alt-arrow-right-outline size-5 />
|
||||
</Transition>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import HeaderLink from './HeaderLink.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header mb-1 w-full gap-2>
|
||||
<div w-full flex translate-x--2 justify-center>
|
||||
<HeaderLink />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -4,11 +4,20 @@ import type { ChatProvider } from '@xsai-ext/shared-providers'
|
||||
import { useMicVAD } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { BasicTextarea } from '@proj-airi/ui'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import MobileChatHistory from '../Widgets/MobileChatHistory.vue'
|
||||
import ActionViewControls from './InteractiveArea/Actions/ViewControls.vue'
|
||||
import ViewControlInputs from './ViewControls/Inputs.vue'
|
||||
|
||||
const isDark = useDark({ disableTransition: false })
|
||||
|
||||
const viewControlsActiveMode = ref<'x' | 'y' | 'z' | 'scale'>('scale')
|
||||
const viewControlsInputsRef = useTemplateRef<InstanceType<typeof ViewControlInputs>>('viewControlsInputs')
|
||||
|
||||
const messageInput = ref('')
|
||||
const listening = ref(false)
|
||||
@@ -19,7 +28,7 @@ const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
|
||||
// const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
|
||||
// const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
|
||||
const { isAudioInputOn, selectedAudioDeviceId, themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
const { isAudioInputOn, selectedAudioDeviceId, themeColorsHueDynamic, stageView, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const { send, onAfterSend, discoverToolsCompatibility } = useChatStore()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { t } = useI18n()
|
||||
@@ -100,34 +109,75 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div fixed bottom-0 w-full flex gap-1 max-w="[calc(100dvw-1rem)]">
|
||||
<MobileChatHistory absolute left-0 top-0 transform="translate-y-[-100%]" h="80dvh" w-full />
|
||||
<div flex flex-1 gap-1>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
:placeholder="t('stage.message')"
|
||||
border="solid 2 primary-50 dark:primary-950/10"
|
||||
text="primary-500 hover:primary-600 dark:primary-100 dark:hover:primary-200 placeholder:primary-400 placeholder:hover:primary-500 placeholder:dark:primary-300 placeholder:dark:hover:primary-400"
|
||||
bg="primary-50/80 dark:primary-950/80"
|
||||
max-h="[10lh]" min-h="[calc(1lh+4px+4px)]"
|
||||
w-full resize-none overflow-y-scroll rounded="[1lh]" px-4 py-0.5 outline-none backdrop-blur-md scrollbar-none
|
||||
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
|
||||
:class="[themeColorsHueDynamic ? 'transition-colors-none placeholder:transition-colors-none' : '']"
|
||||
default-height="1lh"
|
||||
@submit="handleSend"
|
||||
@compositionstart="isComposing = true"
|
||||
@compositionend="isComposing = false"
|
||||
/>
|
||||
<button
|
||||
<div fixed bottom-0 w-full flex flex-col>
|
||||
<KeepAlive>
|
||||
<Transition name="fade">
|
||||
<MobileChatHistory v-if="!stageViewControlsEnabled" max-w="[calc(100%-3.5rem)]" w-full self-start pl-2 />
|
||||
</Transition>
|
||||
</KeepAlive>
|
||||
<div relative w-full self-end>
|
||||
<div fixed top="50%" translate-y="[-50%]" px-2>
|
||||
<ViewControlInputs ref="viewControlsInputs" :mode="viewControlsActiveMode" />
|
||||
</div>
|
||||
<div translate-y="[-100%]" absolute right-0 w-full px-2 pb-2 font-sans>
|
||||
<div flex="~ col" w-full gap-1>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="About">
|
||||
<div i-solar:info-circle-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Theme" @click="isDark = !isDark">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="isDark" i-solar:moon-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
<div v-else i-solar:sun-2-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</Transition>
|
||||
</button>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Language">
|
||||
<div i-solar:earth-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
<RouterLink to="/settings" border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Settings">
|
||||
<div i-solar:settings-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</RouterLink>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Model">
|
||||
<div i-solar:face-scan-circle-outline size-5 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
<button border="2 solid neutral-100/60 dark:neutral-800/30" bg="neutral-50/70 dark:neutral-800/70" w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md title="Stage View" @click="stageView = stageView === '2d' ? '3d' : '2d'">
|
||||
<div v-if="stageView === '2d'" size-5 text="neutral-500 dark:neutral-400" inline-flex items-center justify-center>
|
||||
<span>2D</span>
|
||||
</div>
|
||||
<div v-if="stageView === '3d'" size-5 text="neutral-500 dark:neutral-400" inline-flex items-center justify-center>
|
||||
<span>3D</span>
|
||||
</div>
|
||||
</button>
|
||||
<ActionViewControls v-model="viewControlsActiveMode" @reset="() => viewControlsInputsRef?.resetOnMode()" />
|
||||
</div>
|
||||
</div>
|
||||
<div bg="white dark:neutral-800" w-full flex gap-1 px-2 py-2>
|
||||
<BasicTextarea
|
||||
v-model="messageInput"
|
||||
:placeholder="t('stage.message')"
|
||||
border="solid 2 primary-50 dark:primary-950/10"
|
||||
text="neutral-500 hover:neutral-600 dark:neutral-100 dark:hover:neutral-200 placeholder:neutral-400 placeholder:hover:neutral-500 placeholder:dark:neutral-300 placeholder:dark:hover:neutral-400"
|
||||
bg="neutral-100/80 dark:neutral-950/80"
|
||||
max-h="[10lh]" min-h="[calc(1lh+4px+4px)]"
|
||||
w-full resize-none overflow-y-scroll rounded="[1lh]" px-4 py-0.5 outline-none backdrop-blur-md scrollbar-none
|
||||
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
|
||||
:class="[themeColorsHueDynamic ? 'transition-colors-none placeholder:transition-colors-none' : '']"
|
||||
default-height="1lh"
|
||||
@submit="handleSend"
|
||||
@compositionstart="isComposing = true"
|
||||
@compositionend="isComposing = false"
|
||||
/>
|
||||
<button
|
||||
|
||||
v-if="messageInput.trim() || isComposing"
|
||||
w="[calc(1lh+4px+4px)]" h="[calc(1lh+4px+4px)]" aspect-square flex items-center self-end justify-center rounded-full outline-none backdrop-blur-md
|
||||
text="primary-500 hover:primary-600 dark:primary-100 dark:hover:primary-200 placeholder:primary-400 placeholder:hover:primary-500 placeholder:dark:primary-300 placeholder:dark:hover:primary-400"
|
||||
bg="primary-50/80 dark:primary-950/80"
|
||||
@click="handleSend"
|
||||
>
|
||||
<div i-solar:arrow-up-outline />
|
||||
</button>
|
||||
v-if="messageInput.trim() || isComposing"
|
||||
w="[calc(1lh+4px+4px)]" h="[calc(1lh+4px+4px)]" aspect-square flex items-center self-end justify-center rounded-full outline-none backdrop-blur-md
|
||||
text="neutral-500 hover:neutral-600 dark:neutral-900 dark:hover:neutral-800"
|
||||
bg="primary-50/80 dark:neutral-100/80 hover:neutral-50"
|
||||
transition="all duration-250 ease-in-out"
|
||||
@click="handleSend"
|
||||
>
|
||||
<div i-solar:arrow-up-outline />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { useLive2d, useSettings, useVRM } from '@proj-airi/stage-ui/stores'
|
||||
import { RoundRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'x' | 'y' | 'z' | 'scale'
|
||||
}>()
|
||||
|
||||
const { stageView, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const { scale: vrmScale, position: vrmPosition, modelSize: vrmModelSize } = storeToRefs(useVRM())
|
||||
const { scale: live2dScale, position: live2dPosition } = storeToRefs(useLive2d())
|
||||
|
||||
const viewControlsValueX = computed({
|
||||
get: () => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
return live2dPosition.value.x
|
||||
case '3d':
|
||||
return vrmPosition.value.x
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
live2dPosition.value.x = value
|
||||
break
|
||||
case '3d':
|
||||
vrmPosition.value.x = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueXMin = computed(() => {
|
||||
return stageView.value === '2d' ? -500 : -vrmModelSize.value.x - 10
|
||||
})
|
||||
|
||||
const viewControlsValueXMax = computed(() => {
|
||||
return stageView.value === '2d' ? 500 : vrmModelSize.value.x + 10
|
||||
})
|
||||
|
||||
const viewControlsValueY = computed({
|
||||
get: () => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
return live2dPosition.value.y
|
||||
case '3d':
|
||||
return vrmPosition.value.y
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
live2dPosition.value.y = value
|
||||
break
|
||||
case '3d':
|
||||
vrmPosition.value.y = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueYMin = computed(() => {
|
||||
return stageView.value === '2d' ? -500 : -vrmModelSize.value.y - 10
|
||||
})
|
||||
|
||||
const viewControlsValueYMax = computed(() => {
|
||||
return stageView.value === '2d' ? 500 : vrmModelSize.value.y + 10
|
||||
})
|
||||
|
||||
const viewControlsValueZ = computed({
|
||||
get: () => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
return 0
|
||||
case '3d':
|
||||
return vrmPosition.value.z
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageView.value) {
|
||||
case '2d':
|
||||
break
|
||||
case '3d':
|
||||
vrmPosition.value.z = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueZMin = computed(() => {
|
||||
return stageView.value === '2d' ? -500 : -vrmModelSize.value.z - 10
|
||||
})
|
||||
|
||||
const viewControlsValueZMax = computed(() => {
|
||||
return stageView.value === '2d' ? 500 : vrmModelSize.value.z + 10
|
||||
})
|
||||
|
||||
const viewControlsValueScale = computed({
|
||||
get: () => {
|
||||
if (stageView.value === '2d') {
|
||||
return live2dScale.value
|
||||
}
|
||||
|
||||
return vrmScale.value
|
||||
},
|
||||
set: (value) => {
|
||||
if (stageView.value === '2d') {
|
||||
live2dScale.value = value
|
||||
}
|
||||
else {
|
||||
vrmScale.value = value
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function resetOnMode() {
|
||||
switch (props.mode) {
|
||||
case 'x':
|
||||
viewControlsValueX.value = 0
|
||||
break
|
||||
case 'y':
|
||||
viewControlsValueY.value = 0
|
||||
break
|
||||
case 'z':
|
||||
viewControlsValueZ.value = 0
|
||||
break
|
||||
case 'scale':
|
||||
viewControlsValueScale.value = 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetOnMode,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade-side-pops-in">
|
||||
<div v-if="stageViewControlsEnabled">
|
||||
<Transition name="fade-side-pops-in" mode="out-in">
|
||||
<div v-if="props.mode === 'x'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueX" :min="viewControlsValueXMin" :max="viewControlsValueXMax" :step="0.01" data-direction="vertical" h="50%" write-vertical-left />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueX.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.mode === 'y'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueY" :min="viewControlsValueYMin" :max="viewControlsValueYMax" :step="0.01" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueY.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="stageView === '3d' && props.mode === 'z'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueZ" :min="viewControlsValueZMin" :max="viewControlsValueZMax" :step="0.01" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueZ.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.mode === 'scale'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueScale" :min="0" :max="3" :step="0.0001" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueScale.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-side-pops-in-enter-active,
|
||||
.fade-side-pops-in-leave-active {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-side-pops-in-enter-from,
|
||||
.fade-side-pops-in-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%) scale(0.8);
|
||||
}
|
||||
|
||||
.fade-side-pops-in-enter-to,
|
||||
.fade-side-pops-in-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMarkdown } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useElementBounding, useScroll } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -10,8 +9,6 @@ const chatHistoryRef = ref<HTMLDivElement>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { messages, sending } = storeToRefs(useChatStore())
|
||||
const bounding = useElementBounding(chatHistoryRef, { immediate: true, windowScroll: true, windowResize: true })
|
||||
const { y: chatHistoryContainerY } = useScroll(chatHistoryRef)
|
||||
|
||||
const { process } = useMarkdown()
|
||||
const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
@@ -19,16 +16,20 @@ const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
onBeforeMessageComposed(async () => {
|
||||
// Scroll down to the new sent message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
if (!chatHistoryRef.value)
|
||||
return
|
||||
|
||||
chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight
|
||||
})
|
||||
})
|
||||
|
||||
onTokenLiteral(async () => {
|
||||
// Scroll down to the new responding message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
if (!chatHistoryRef.value)
|
||||
return
|
||||
|
||||
chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMarkdown } from '@proj-airi/stage-ui/composables'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useElementBounding, useScroll } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -10,8 +9,6 @@ const chatHistoryRef = ref<HTMLDivElement>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const bounding = useElementBounding(chatHistoryRef, { immediate: true, windowScroll: true, windowResize: true })
|
||||
const { y: chatHistoryContainerY } = useScroll(chatHistoryRef)
|
||||
|
||||
const { process } = useMarkdown()
|
||||
const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
@@ -19,25 +16,27 @@ const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
|
||||
onBeforeMessageComposed(async () => {
|
||||
// Scroll down to the new sent message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
if (!chatHistoryRef.value)
|
||||
return
|
||||
|
||||
chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight
|
||||
})
|
||||
})
|
||||
|
||||
onTokenLiteral(async () => {
|
||||
// Scroll down to the new responding message
|
||||
nextTick().then(() => {
|
||||
bounding.update()
|
||||
chatHistoryContainerY.value = bounding.height.value
|
||||
if (!chatHistoryRef.value)
|
||||
return
|
||||
|
||||
chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div py="1" flex="~ col" rounded="lg" overflow-hidden>
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div ref="chatHistoryRef" v-auto-animate h-full w-full max-h="40%" flex="~ col" overflow-scroll class="chat-history">
|
||||
<div flex-1 /> <!-- spacer -->
|
||||
<div flex="~ col" rounded="lg" overflow-hidden>
|
||||
<div ref="chatHistoryRef" v-auto-animate max-h="35dvh" z-5 flex="~ col" h-full w-full overflow-scroll class="chat-history">
|
||||
<div v-for="(message, index) in messages" :key="index" mb-2>
|
||||
<div v-if="message.role === 'error'" flex mr="12">
|
||||
<div
|
||||
|
||||
@@ -138,7 +138,7 @@ onMounted(() => updateThemeColor())
|
||||
<HeaderLink />
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="h-full px-3 py-0 sm:h-[calc(100%-4rem)] md:py-0 xl:px-0" flex="~ col" mx-auto max-w-screen-xl>
|
||||
<div class="h-[calc(100%-40px)] px-3 py-0 2xl:max-w-[2160px] sm:h-[calc(100%-56px)] md:py-0 xl:px-4" flex="~ col" mx-auto>
|
||||
<PageHeader
|
||||
:title="routeHeaderMetadata?.title"
|
||||
:subtitle="routeHeaderMetadata?.subtitle"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { onMounted, ref, watch } from 'vue'
|
||||
import Cross from '../components/Backgrounds/Cross.vue'
|
||||
import Header from '../components/Layouts/Header.vue'
|
||||
import InteractiveArea from '../components/Layouts/InteractiveArea.vue'
|
||||
import MobileHeader from '../components/Layouts/MobileHeader.vue'
|
||||
import MobileInteractiveArea from '../components/Layouts/MobileInteractiveArea.vue'
|
||||
import AnimatedWave from '../components/Widgets/AnimatedWave.vue'
|
||||
|
||||
@@ -41,7 +42,8 @@ onMounted(() => updateThemeColor())
|
||||
<div relative flex="~ col" z-2 h-100dvh w-100vw of-hidden>
|
||||
<!-- header -->
|
||||
<div class="px-0 py-1 md:px-3 md:py-3" w-full gap-2>
|
||||
<Header class="flex" />
|
||||
<Header class="hidden md:flex" />
|
||||
<MobileHeader class="flex md:hidden" />
|
||||
</div>
|
||||
<!-- page -->
|
||||
<div relative flex="~ 1 row gap-y-0 gap-x-2 <md:col">
|
||||
@@ -56,8 +58,8 @@ onMounted(() => updateThemeColor())
|
||||
:y-offset="positionInPercentageString.y"
|
||||
:scale="scale"
|
||||
/>
|
||||
<InteractiveArea class="flex <md:hidden" absolute h="85dvh" right-4 flex-1 max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea class="hidden <md:block" mx2 mb2 @settings-open="handleSettingsOpen" />
|
||||
<InteractiveArea v-if="!isMobile" h="85dvh" absolute right-4 flex flex-1 flex-col max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea v-if="isMobile" @settings-open="handleSettingsOpen" />
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedWave>
|
||||
|
||||
@@ -43,8 +43,14 @@ const {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex class="relative h-[calc(100dvh-8rem)] flex-col-reverse md:flex-row">
|
||||
<ModelSettings live-2d-settings-class="w-100% md:w-40% lg:w-30% xl:w-30% 2xl:w-30% h-fit overflow-y-scroll absolute bottom-0 right-0 top-0" :palette="palette" @extract-colors-from-model="extractColorsFromModel" />
|
||||
<div flex class="relative h-full flex-col-reverse md:flex-row">
|
||||
<ModelSettings
|
||||
live-2d-scene-class="absolute max-h-[calc(100dvh-100px-56px)] w-full h-full"
|
||||
live-2d-settings-class="w-100% md:w-40% lg:w-30% xl:w-30% 2xl:w-30% h-fit sm:max-h-80dvh overflow-y-scroll relative"
|
||||
vrm-scene-class="absolute max-h-[calc(100dvh-100px-56px)] w-full h-full"
|
||||
vrm-settings-class="w-100% md:w-40% lg:w-30% xl:w-30% 2xl:w-30% h-fit sm:max-h-80dvh overflow-y-scroll relative"
|
||||
:palette="palette" @extract-colors-from-model="extractColorsFromModel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IconAnimation
|
||||
@@ -58,6 +64,7 @@ const {
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-motion
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
|
||||
import { useDark, useMouse } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import { useLive2d } from '@proj-airi/stage-ui/stores'
|
||||
import { breakpointsTailwind, useBreakpoints, useDark, useMouse } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import Cross from '../../components/Backgrounds/Cross.vue'
|
||||
import Header from '../../components/Layouts/Header.vue'
|
||||
import InteractiveArea from '../../components/Layouts/InteractiveArea.vue'
|
||||
import MobileHeader from '../../components/Layouts/MobileHeader.vue'
|
||||
import MobileInteractiveArea from '../../components/Layouts/MobileInteractiveArea.vue'
|
||||
import AnimatedWave from '../../components/Widgets/AnimatedWave.vue'
|
||||
|
||||
import { themeColorFromPropertyOf, useThemeColor } from '../../composables/theme-color'
|
||||
|
||||
const dark = useDark()
|
||||
const paused = ref(false)
|
||||
|
||||
@@ -17,19 +22,28 @@ function handleSettingsOpen(open: boolean) {
|
||||
}
|
||||
|
||||
const positionCursor = useMouse()
|
||||
const { scale, position, positionInPercentageString } = storeToRefs(useLive2d())
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isMobile = breakpoints.smaller('md')
|
||||
|
||||
const { updateThemeColor } = useThemeColor(themeColorFromPropertyOf('.widgets.top-widgets .colored-area', 'background-color'))
|
||||
watch(dark, () => updateThemeColor(), { immediate: true })
|
||||
onMounted(() => updateThemeColor())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Cross>
|
||||
<AnimatedWave
|
||||
class="widgets top-widgets"
|
||||
:fill-color="dark
|
||||
? 'oklch(35% calc(var(--chromatic-chroma) * 0.6) var(--chromatic-hue))'
|
||||
: 'color-mix(in srgb, oklch(95% calc(var(--chromatic-chroma-50) * 0.5) var(--chromatic-hue)) 80%, oklch(100% 0 360))'"
|
||||
>
|
||||
<div relative flex="~ col" z-2 h-100dvh w-100vw of-hidden>
|
||||
<!-- header -->
|
||||
<div>
|
||||
<Header class="flex" p2 />
|
||||
<div class="px-0 py-1 md:px-3 md:py-3" w-full gap-2>
|
||||
<Header class="hidden md:flex" />
|
||||
<MobileHeader class="flex md:hidden" />
|
||||
</div>
|
||||
<!-- page -->
|
||||
<div relative flex="~ 1 row gap-y-0 gap-x-2 <md:col">
|
||||
@@ -40,9 +54,12 @@ const positionCursor = useMouse()
|
||||
x: positionCursor.x.value,
|
||||
y: positionCursor.y.value,
|
||||
}"
|
||||
:x-offset="`${isMobile ? position.x : position.x - 10}%`"
|
||||
:y-offset="positionInPercentageString.y"
|
||||
:scale="scale"
|
||||
/>
|
||||
<InteractiveArea class="flex <md:hidden" flex-1 max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea class="hidden <md:block" mx2 mb2 @settings-open="handleSettingsOpen" />
|
||||
<InteractiveArea v-if="!isMobile" h="85dvh" absolute right-4 flex flex-1 flex-col max-w="500px" min-w="30%" />
|
||||
<MobileInteractiveArea v-if="isMobile" @settings-open="handleSettingsOpen" />
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedWave>
|
||||
@@ -50,7 +67,7 @@ const positionCursor = useMouse()
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
name: StageScenePage
|
||||
name: IndexScenePage
|
||||
meta:
|
||||
layout: stage
|
||||
stageTransition:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import './transitions.css';
|
||||
@import './vue-transitions.css';
|
||||
|
||||
:root {
|
||||
--bg-color-light: rgb(255 255 255);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
.fade-slide-out-r-to-l-enter-active,
|
||||
.fade-slide-out-r-to-l-leave-active {
|
||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-slide-out-r-to-l-enter-from,
|
||||
.fade-slide-out-r-to-l-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
.fade-slide-out-r-to-l-enter-to,
|
||||
.fade-slide-out-r-to-l-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.fade-slide-out-l-to-r-enter-active,
|
||||
.fade-slide-out-l-to-r-leave-active {
|
||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-slide-out-l-to-r-enter-from,
|
||||
.fade-slide-out-l-to-r-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.fade-slide-out-l-to-r-enter-to,
|
||||
.fade-slide-out-l-to-r-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
Reference in New Issue
Block a user