diff --git a/apps/stage-tamagotchi/src/renderer/components/ChatHistory.vue b/apps/stage-tamagotchi/src/renderer/components/ChatHistory.vue
deleted file mode 100644
index abe1fab98..000000000
--- a/apps/stage-tamagotchi/src/renderer/components/ChatHistory.vue
+++ /dev/null
@@ -1,207 +0,0 @@
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.core-system') }}
-
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.airi') }}
-
-
-
-
-
-
-
-
-
-
- {{ JSON.stringify(JSON.parse(slice.toolCall.args), null, 2).trim() }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.you') }}
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.airi') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ JSON.stringify(JSON.parse(slice.toolCall.args), null, 2).trim() }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue b/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue
index cb4819d87..dd05f588a 100644
--- a/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue
+++ b/apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue
@@ -1,6 +1,7 @@
@@ -23,8 +26,16 @@ const isLoading = ref(true)
>
-
-
+
+
diff --git a/apps/stage-web/src/components/Layouts/MobileInteractiveArea.vue b/apps/stage-web/src/components/Layouts/MobileInteractiveArea.vue
index beea26c01..7bc6add6f 100644
--- a/apps/stage-web/src/components/Layouts/MobileInteractiveArea.vue
+++ b/apps/stage-web/src/components/Layouts/MobileInteractiveArea.vue
@@ -2,6 +2,7 @@
import type { ChatProvider } from '@xsai-ext/shared-providers'
import { HearingConfigDialog } from '@proj-airi/stage-ui/components'
+import { ChatHistory } from '@proj-airi/stage-ui/components/scenarios/chat'
import { useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
@@ -16,13 +17,13 @@ import { useI18n } from 'vue-i18n'
import { RouterLink } from 'vue-router'
import IndicatorMicVolume from '../Widgets/IndicatorMicVolume.vue'
-import MobileChatHistory from '../Widgets/MobileChatHistory.vue'
import ActionAbout from './InteractiveArea/Actions/About.vue'
import ActionViewControls from './InteractiveArea/Actions/ViewControls.vue'
import ViewControlInputs from './ViewControls/Inputs.vue'
const { isDark, toggleDark } = useTheme()
const hearingDialogOpen = ref(false)
+const { messages, sending, streamingMessage } = storeToRefs(useChatStore())
const viewControlsActiveMode = ref<'x' | 'y' | 'z' | 'scale'>('scale')
const viewControlsInputsRef = useTemplateRef
>('viewControlsInputs')
@@ -39,7 +40,6 @@ const { themeColorsHueDynamic, stageViewControlsEnabled } = storeToRefs(useSetti
const settingsAudioDevice = useSettingsAudioDevice()
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(settingsAudioDevice)
const { send, onAfterMessageComposed, discoverToolsCompatibility, cleanupMessages } = useChatStore()
-const { messages } = storeToRefs(useChatStore())
const { t } = useI18n()
const { audioContext } = useAudioContext()
const { startAnalyzer, stopAnalyzer, volumeLevel } = useAudioAnalyzer()
@@ -133,7 +133,21 @@ onMounted(() => {
-
+
@@ -220,3 +234,37 @@ onMounted(() => {
+
+
diff --git a/apps/stage-web/src/components/Widgets/ChatArea.vue b/apps/stage-web/src/components/Widgets/ChatArea.vue
index 40efd3157..7dd2ffe45 100644
--- a/apps/stage-web/src/components/Widgets/ChatArea.vue
+++ b/apps/stage-web/src/components/Widgets/ChatArea.vue
@@ -116,7 +116,7 @@ onUnmounted(() => {
-import { MarkdownRenderer } from '@proj-airi/stage-ui/components'
-import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
-import { storeToRefs } from 'pinia'
-import { ref, watch } from 'vue'
-import { useI18n } from 'vue-i18n'
-
-const chatHistoryRef = ref()
-
-const { t } = useI18n()
-const { messages, sending, streamingMessage } = storeToRefs(useChatStore())
-
-const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
-
-function scrollToBottom() {
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- if (!chatHistoryRef.value)
- return
-
- chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight
- })
- })
-}
-
-onBeforeMessageComposed(async () => {
- // Scroll down to the new sent message
- await scrollToBottom()
-})
-
-onTokenLiteral(async () => {
- // Scroll down to the new responding message
- await scrollToBottom()
-})
-
-watch(sending, () => {
- scrollToBottom()
-}, { flush: 'post' })
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.core-system') }}
-
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.airi') }}
-
-
-
-
-
- Called: {{ slice.toolCall.toolName }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.you') }}
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.airi') }}
-
-
-
-
-
- Called: {{ slice.toolCall.toolName }}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/stage-web/src/components/Widgets/MobileChatHistory.vue b/apps/stage-web/src/components/Widgets/MobileChatHistory.vue
deleted file mode 100644
index 0202da632..000000000
--- a/apps/stage-web/src/components/Widgets/MobileChatHistory.vue
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.core-system') }}
-
-
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.airi') }}
-
-
-
-
-
-
-
-
- {{ t('stage.chat.message.character-name.you') }}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/stage-ui/histoire.config.ts b/packages/stage-ui/histoire.config.ts
index 3ab8833f5..34e227676 100644
--- a/packages/stage-ui/histoire.config.ts
+++ b/packages/stage-ui/histoire.config.ts
@@ -112,6 +112,10 @@ export default defineConfig({
id: 'widgets',
title: 'Widgets',
},
+ {
+ id: 'chat',
+ title: 'Chat',
+ },
{
id: 'gadgets',
title: 'Gadgets',
diff --git a/packages/stage-ui/src/components/layout/ripple-grid/RippleGrid.story.vue b/packages/stage-ui/src/components/layout/ripple-grid/RippleGrid.story.vue
index 0a84e70c8..7eee58028 100644
--- a/packages/stage-ui/src/components/layout/ripple-grid/RippleGrid.story.vue
+++ b/packages/stage-ui/src/components/layout/ripple-grid/RippleGrid.story.vue
@@ -28,7 +28,7 @@ function handleItemClick({ globalIndex }: { globalIndex: number }) {
+import type { ChatAssistantMessage, ChatSlices, ChatSlicesText } from '../../../types/chat'
+
+import { computed } from 'vue'
+
+import MarkdownRenderer from '../../markdown/MarkdownRenderer.vue'
+import ChatToolCallBlock from './ChatToolCallBlock.vue'
+
+const props = withDefaults(defineProps<{
+ message: ChatAssistantMessage
+ label: string
+ showPlaceholder?: boolean
+ variant?: 'desktop' | 'mobile'
+}>(), {
+ showPlaceholder: false,
+ variant: 'desktop',
+})
+
+const resolvedSlices = computed(() => {
+ if (props.message.slices?.length) {
+ return props.message.slices
+ }
+
+ if (typeof props.message.content === 'string' && props.message.content.trim()) {
+ return [{ type: 'text', text: props.message.content } satisfies ChatSlicesText]
+ }
+
+ if (Array.isArray(props.message.content)) {
+ const textPart = props.message.content.find(part => 'type' in part && part.type === 'text') as { text?: string } | undefined
+ if (textPart?.text)
+ return [{ type: 'text', text: textPart.text } satisfies ChatSlicesText]
+ }
+
+ return []
+})
+
+const showLoader = computed(() => props.showPlaceholder && resolvedSlices.value.length === 0)
+const containerClass = computed(() => props.variant === 'mobile' ? 'mr-0' : 'mr-12')
+const boxClasses = computed(() => [
+ props.variant === 'mobile' ? 'px-2 py-2 text-sm bg-primary-50/90 dark:bg-primary-950/90' : 'px-3 py-3 bg-primary-50/80 dark:bg-primary-950/80',
+])
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/stage-ui/src/components/scenarios/chat/ChatErrorItem.vue b/packages/stage-ui/src/components/scenarios/chat/ChatErrorItem.vue
new file mode 100644
index 000000000..df74af585
--- /dev/null
+++ b/packages/stage-ui/src/components/scenarios/chat/ChatErrorItem.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
diff --git a/packages/stage-ui/src/components/scenarios/chat/ChatHistory.story.vue b/packages/stage-ui/src/components/scenarios/chat/ChatHistory.story.vue
new file mode 100644
index 000000000..9aa08e656
--- /dev/null
+++ b/packages/stage-ui/src/components/scenarios/chat/ChatHistory.story.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/stage-ui/src/components/scenarios/chat/ChatHistory.vue b/packages/stage-ui/src/components/scenarios/chat/ChatHistory.vue
new file mode 100644
index 000000000..05dfd3f70
--- /dev/null
+++ b/packages/stage-ui/src/components/scenarios/chat/ChatHistory.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/stage-ui/src/components/scenarios/chat/ChatToolCallBlock.vue b/packages/stage-ui/src/components/scenarios/chat/ChatToolCallBlock.vue
new file mode 100644
index 000000000..dc1ccb013
--- /dev/null
+++ b/packages/stage-ui/src/components/scenarios/chat/ChatToolCallBlock.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+ {{ formattedArgs }}
+
+
+
+
diff --git a/packages/stage-ui/src/components/scenarios/chat/ChatUserItem.vue b/packages/stage-ui/src/components/scenarios/chat/ChatUserItem.vue
new file mode 100644
index 000000000..6ae8e9941
--- /dev/null
+++ b/packages/stage-ui/src/components/scenarios/chat/ChatUserItem.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
diff --git a/packages/stage-ui/src/components/scenarios/chat/index.ts b/packages/stage-ui/src/components/scenarios/chat/index.ts
new file mode 100644
index 000000000..7cf8f07e8
--- /dev/null
+++ b/packages/stage-ui/src/components/scenarios/chat/index.ts
@@ -0,0 +1,6 @@
+export { default as ChatAssistantItem } from './ChatAssistantItem.vue'
+export { default as ChatErrorItem } from './ChatErrorItem.vue'
+export { default as ChatHistory } from './ChatHistory.vue'
+export { default as ChatUserItem } from './ChatUserItem.vue'
+
+export type { ChatErrorMessage, ChatHistoryMessage } from './types'
diff --git a/packages/stage-ui/src/components/scenarios/chat/types.ts b/packages/stage-ui/src/components/scenarios/chat/types.ts
new file mode 100644
index 000000000..d03d6a166
--- /dev/null
+++ b/packages/stage-ui/src/components/scenarios/chat/types.ts
@@ -0,0 +1,10 @@
+import type { ChatAssistantMessage, ChatMessage, ChatSlices } from '../../../types/chat'
+
+export interface ChatErrorMessage {
+ role: 'error'
+ content: string
+}
+
+export type ChatHistoryMessage = (ChatMessage | ChatErrorMessage) & {
+ slices?: ChatSlices[]
+}
diff --git a/packages/stage-ui/src/components/scenarios/index.ts b/packages/stage-ui/src/components/scenarios/index.ts
index e95f7dbe9..878a0d7f9 100644
--- a/packages/stage-ui/src/components/scenarios/index.ts
+++ b/packages/stage-ui/src/components/scenarios/index.ts
@@ -1,4 +1,5 @@
export * from './dialogs'
+export * from './chat'
export * from './providers'
export * from './settings'
export * from './toasters'