-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* rebase origin main * exclude packages/desktop/out * tmp commit * use mobile ui instead of desktop ui * fix lockfile * send ipc event to move window * load zip file * rename folder * remove `less` * add correct prefix for script * use airi logo instead of electron default logo * remove duplicate icon * use import.meta instead of __dirname
- Loading branch information
1 parent
1a773e6
commit 3c8e778
Showing
35 changed files
with
2,985 additions
and
377 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
packages/stage/src/components/Layouts/TamagotchiInteractiveArea.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
<script setup lang="ts"> | ||
// import { useDevicesList } from '@vueuse/core' | ||
import { storeToRefs } from 'pinia' | ||
import { DrawerContent, DrawerPortal, DrawerRoot, DrawerTrigger } from 'vaul-vue' | ||
import { onMounted, ref, watch } from 'vue' | ||
import { useI18n } from 'vue-i18n' | ||
import { useMicVAD } from '../../composables/micvad' | ||
// import { useAudioContext } from '../../stores/audio' | ||
import { useChatStore } from '../../stores/chat' | ||
import { useSettings } from '../../stores/settings' | ||
import BasicTextarea from '../BasicTextarea.vue' | ||
import TamagotchiChatHistory from '../Widgets/TamagotchiChatHistory.vue' | ||
import TamagotchiSettings from '../Widgets/TamagotchiSettings.vue' | ||
const messageInput = ref('') | ||
const listening = ref(false) | ||
// const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true }) | ||
// const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings()) | ||
const { isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings()) | ||
const { send, onAfterSend } = useChatStore() | ||
const { t } = useI18n() | ||
async function handleSend() { | ||
if (!messageInput.value.trim()) { | ||
return | ||
} | ||
await send(messageInput.value) | ||
} | ||
const { destroy, start } = useMicVAD(selectedAudioDeviceId, { | ||
onSpeechStart: () => { | ||
// TODO: interrupt the playback | ||
// TODO: interrupt any of the ongoing TTS | ||
// TODO: interrupt any of the ongoing LLM requests | ||
// TODO: interrupt any of the ongoing animation of Live2D or VRM | ||
// TODO: once interrupted, we should somehow switch to listen or thinking | ||
// emotion / expression? | ||
listening.value = true | ||
}, | ||
// VAD misfire means while speech end is detected but | ||
// the frames of the segment of the audio buffer | ||
// is not enough to be considered as a speech segment | ||
// which controlled by the `minSpeechFrames` parameter | ||
onVADMisfire: () => { | ||
// TODO: do audio buffer send to whisper | ||
listening.value = false | ||
}, | ||
onSpeechEnd: (buffer) => { | ||
// TODO: do audio buffer send to whisper | ||
listening.value = false | ||
handleTranscription(buffer) | ||
}, | ||
auto: false, | ||
}) | ||
function handleTranscription(_buffer: Float32Array<ArrayBufferLike>) { | ||
// eslint-disable-next-line no-alert | ||
alert('Transcription is not implemented yet') | ||
} | ||
// 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() | ||
} | ||
}) | ||
onAfterSend(async () => { | ||
messageInput.value = '' | ||
}) | ||
onMounted(() => { | ||
start() | ||
}) | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<div relative w-full flex gap-1> | ||
<TamagotchiChatHistory absolute left-0 top-0 transform="translate-y-[-100%]" w-full /> | ||
<div flex flex-1> | ||
<BasicTextarea | ||
v-model="messageInput" | ||
:placeholder="t('stage.message')" | ||
border="solid 2 pink-100" | ||
text="pink-400 hover:pink-600 placeholder:pink-400 placeholder:hover:pink-600" | ||
bg="pink-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]" | ||
w-full resize-none overflow-y-scroll rounded-l-xl p-2 font-medium outline-none | ||
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out" | ||
@submit="handleSend" | ||
/> | ||
</div> | ||
<DrawerRoot should-scale-background> | ||
<DrawerTrigger | ||
class="px-4 py-2.5" | ||
border="solid 2 pink-100 " | ||
text="lg pink-400 hover:pink-600 placeholder:pink-400 placeholder:hover:pink-600" | ||
bg="pink-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]" rounded-r-xl | ||
> | ||
<div i-solar:settings-bold-duotone /> | ||
</DrawerTrigger> | ||
<DrawerPortal> | ||
<DrawerContent | ||
max-h="[90%]" | ||
fixed bottom-0 left-0 right-0 z-50 mt-24 h-full flex flex-col rounded-t-lg bg="[#fffbff] dark:[#1f1a1d]" | ||
> | ||
<div class="flex flex-1 flex-col rounded-t-lg p-5" bg="[#fffbff] dark:[#1f1a1d]" gap-2> | ||
<TamagotchiSettings /> | ||
</div> | ||
</DrawerContent> | ||
</DrawerPortal> | ||
</DrawerRoot> | ||
</div> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
packages/stage/src/components/Widgets/TamagotchiChatHistory.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<script setup lang="ts"> | ||
import { useElementBounding, useScroll } from '@vueuse/core' | ||
import { storeToRefs } from 'pinia' | ||
import { nextTick, ref } from 'vue' | ||
import { useMarkdown } from '../../composables/markdown' | ||
import { useChatStore } from '../../stores/chat' | ||
const chatHistoryRef = ref<HTMLDivElement>() | ||
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() | ||
onBeforeMessageComposed(async () => { | ||
// Scroll down to the new sent message | ||
nextTick().then(() => { | ||
bounding.update() | ||
chatHistoryContainerY.value = bounding.height.value | ||
}) | ||
}) | ||
onTokenLiteral(async () => { | ||
// Scroll down to the new responding message | ||
nextTick().then(() => { | ||
bounding.update() | ||
chatHistoryContainerY.value = bounding.height.value | ||
}) | ||
}) | ||
</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="30vh" flex="~ col" overflow-scroll> | ||
<div flex-1 /> <!-- spacer --> | ||
<div v-for="(message, index) in messages" :key="index" mb-2> | ||
<div v-if="message.role === 'assistant'" flex mr="12"> | ||
<div | ||
flex="~ col" | ||
border="4 solid pink-200" | ||
shadow="md pink-200/50" | ||
min-w-20 rounded-lg px-2 py-1 | ||
h="fit" | ||
bg="pink-100" | ||
> | ||
<div> | ||
<span text-xs text="pink-400/90" font-semibold class="inline hidden">Airi</span> | ||
</div> | ||
<div v-if="message.content" class="markdown-content" text="xs pink-400" v-html="process(message.content as string)" /> | ||
<div v-else i-eos-icons:three-dots-loading /> | ||
</div> | ||
</div> | ||
<div v-else-if="message.role === 'user'" flex="~"> | ||
<div | ||
flex="~ col" | ||
border="4 solid cyan-200" | ||
shadow="md cyan-200/50" | ||
px="2" | ||
h="fit" min-w-20 rounded-lg px-2 py-1 | ||
bg="cyan-100" | ||
> | ||
<div> | ||
<span text-xs text="cyan-600/90" font-semibold class="hidden">You</span> | ||
</div> | ||
<div v-if="message.content" class="markdown-content" text="xs cyan-600" v-html="process(message.content as string)" /> | ||
<div v-else /> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</template> |
Oops, something went wrong.