refactor(tamagotchi): move chat to a standalone window (#174)

* chore(tamagotchi): remove unused dependencies
* fix: sync mcp connection state between windows
* refactor: move chat to a standalone window
This commit is contained in:
LemonNeko
2025-05-25 22:45:31 +08:00
committed by GitHub
parent 5286f06a89
commit 1151a7d731
11 changed files with 75 additions and 45 deletions
Generated
-2
View File
@@ -95,8 +95,6 @@ name = "app"
version = "0.4.27"
dependencies = [
"log",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-log",
@@ -21,8 +21,6 @@ crate-type = [
tauri-build = { version = "2.2.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = [ "derive" ] }
log = "0.4"
tauri = { version = "2.3.1", features = [
'macos-private-api',
@@ -32,6 +32,7 @@ pub async fn open_chat_window(app: tauri::AppHandle) {
WebviewUrl::App(Path::new("#/chat").to_path_buf()),
)
.title("chat")
.inner_size(600.0, 800.0)
.build()
.unwrap();
}
@@ -34,10 +34,10 @@ onTokenLiteral(async () => {
</script>
<template>
<div py="1" flex="~ col" rounded="lg" overflow-hidden>
<div py="1" flex="~ col" h-full rounded="lg" overflow-hidden>
<div flex-1 /> <!-- spacer -->
<div
ref="chatHistoryRef" v-auto-animate max-h="30vh" flex="~ col"
ref="chatHistoryRef" v-auto-animate flex="~ col"
scrollbar="~ w-2 track-color-transparent thumb-color-primary-300 rounded" h-full w-full overflow-scroll
>
<div flex-1 /> <!-- spacer -->
@@ -4,7 +4,6 @@ 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 { invoke } from '@tauri-apps/api/core'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -87,10 +86,6 @@ function handleTranscription(_buffer: Float32Array) {
// selectedAudioDevice.value = found
// }
function openSettings() {
invoke('open_settings_window')
}
watch(isAudioInputOn, async (value) => {
if (value === 'false') {
destroy()
@@ -108,30 +103,20 @@ onMounted(() => {
<template>
<div>
<div relative w-full flex gap-1>
<TamagotchiChatHistory transform="translate-y-[-100%]" absolute left-0 top-0 w-full />
<div flex flex-1>
<BasicTextarea
v-model="messageInput"
:placeholder="t('stage.message')"
border="solid 2 primary-100"
text="primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
bg="primary-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 h-full w-full flex="~ col gap-1">
<div w-full flex-1>
<TamagotchiChatHistory />
</div>
<div
class="px-4 py-2.5"
border="solid 2 primary-100 "
text="lg primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
<BasicTextarea
v-model="messageInput"
:placeholder="t('stage.message')"
border="solid 2 primary-100"
text="primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
bg="primary-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
flex items-center justify-center rounded-r-xl
@click="openSettings"
>
<div i-solar:settings-bold-duotone />
</div>
w-full resize-none overflow-y-scroll rounded-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>
</div>
</template>
@@ -3,7 +3,7 @@ import { RouterView } from 'vue-router'
</script>
<template>
<div px-4 py-8 flex="~ col gap-4">
<div h-full w-full p-4 flex="~ col gap-4">
<RouterView />
</div>
</template>
@@ -106,6 +106,7 @@ const routeHeaderMetadata = computed(() => routeHeaderMetadataMap.value[route.pa
v-if="routeHeaderMetadata"
:title="routeHeaderMetadata.title"
:subtitle="routeHeaderMetadata.subtitle"
:show-back-button="route.path !== '/settings'"
/>
<RouterView />
</div>
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
import InteractiveArea from '../components/InteractiveArea.vue'
</script>
<template>
<InteractiveArea
class="interaction-area block"
h-full w-full transition="opacity duration-250"
/>
</template>
<route lang="yaml">
meta:
layout: stage
</route>
+33 -6
View File
@@ -2,10 +2,10 @@
import { WidgetStage } from '@proj-airi/stage-ui/components'
import { useMcpStore } from '@proj-airi/stage-ui/stores'
import { connectServer } from '@proj-airi/tauri-plugin-mcp'
import { invoke } from '@tauri-apps/api/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted } from 'vue'
import InteractiveArea from '../components/InteractiveArea.vue'
import { useWindowShortcuts } from '../composables/window-shortcuts'
import { useWindowControlStore } from '../stores/window-controls'
import { WindowControlMode } from '../types/window-controls'
@@ -29,6 +29,14 @@ const modeIndicatorClass = computed(() => {
}
})
function openSettings() {
invoke('open_settings_window')
}
function openChat() {
invoke('open_chat_window')
}
onMounted(async () => {
if (connected.value)
return
@@ -58,11 +66,30 @@ onMounted(async () => {
>
<div relative h-full w-full items-end gap-2 class="view">
<WidgetStage h-full w-full flex-1 mb="<md:18" />
<InteractiveArea
class="interaction-area block"
:class="{ 'pointer-events-none': !windowStore.isControlActive }"
absolute bottom-0 w-full transition="opacity duration-250" op-0
/>
<div
absolute bottom-4 left-4 flex gap-1 op-0 transition="opacity duration-250"
class="interaction-area"
:class="{ 'pointer-events-none': windowStore.isControlActive }"
>
<div
border="solid 2 primary-100 "
text="lg primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
bg="primary-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
flex cursor-pointer items-center justify-center rounded-l-xl p-4 transition-colors
@click="openChat"
>
<div i-solar:chat-line-bold-duotone />
</div>
<div
border="solid 2 primary-100 "
text="lg primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
bg="primary-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
flex cursor-pointer items-center justify-center rounded-r-xl p-4 transition-colors
@click="openSettings"
>
<div i-solar:settings-bold-duotone />
</div>
</div>
</div>
<!-- Debug Mode UI -->
@@ -3,10 +3,13 @@ import { useMotion } from '@vueuse/motion'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const props = defineProps<{
const props = withDefaults(defineProps<{
title: string
subtitle?: string
}>()
showBackButton?: boolean
}>(), {
showBackButton: true,
})
const router = useRouter()
const route = useRoute()
@@ -54,7 +57,10 @@ watch([() => props.title, () => props.subtitle, route], async () => {
bg="$bg-color"
>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
<div
i-solar:alt-arrow-left-line-duotone text-2xl
:class="{ 'pointer-events-none op-0': !showBackButton }"
/>
</button>
<h1 relative>
<div v-if="subtitle" absolute left-0 top-0 translate-y="[-80%]">
+1 -2
View File
@@ -1,11 +1,10 @@
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useMcpStore = defineStore('mcp', () => {
const serverCmd = useLocalStorage('settings/mcp/server-cmd', '')
const serverArgs = useLocalStorage('settings/mcp/server-args', '')
const connected = ref(false)
const connected = useLocalStorage('mcp/connected', false) // use local storage to sync between windows
return {
serverCmd,