From e3e0e5474a3ce431e6ffdc94d0016f08ddcad36e Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:32:45 +0800 Subject: [PATCH 01/18] chore: update vue version to ^3.5.13 --- packages/vue/package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vue/package.json b/packages/vue/package.json index fa8e684..92119b7 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -46,6 +46,6 @@ "@xsai-use/shared": "workspace:" }, "devDependencies": { - "vue": "^3.0.0" + "vue": "^3.5.13" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2700b4b..7f0a4ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,7 +205,7 @@ importers: version: link:../shared devDependencies: vue: - specifier: ^3.0.0 + specifier: ^3.5.13 version: 3.5.13(typescript@5.8.3) packages: From 81d4815ecc106b30f9a5bbddf423bfcce38e9072 Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:33:21 +0800 Subject: [PATCH 02/18] feat: add Vue example with chat functionality and update dependencies --- examples/vue/index.html | 12 ++ examples/vue/package.json | 19 +++ examples/vue/src/App.vue | 186 +++++++++++++++++++++ examples/vue/src/MessageBubble.vue | 136 ++++++++++++++++ examples/vue/src/index.ts | 5 + examples/vue/src/style.css | 97 +++++++++++ examples/vue/src/vite-env.d.ts | 1 + examples/vue/vite.config.ts | 8 + package.json | 2 + packages/vue/package.json | 2 +- packages/vue/src/App.vue | 18 --- packages/vue/src/index.ts | 2 + packages/vue/src/use-chat.ts | 251 +++++++++++++++++++++++++++++ pnpm-lock.yaml | 49 ++++++ tsconfig.json | 3 +- tsconfig.vue.json | 19 +++ 16 files changed, 790 insertions(+), 20 deletions(-) create mode 100644 examples/vue/index.html create mode 100644 examples/vue/package.json create mode 100644 examples/vue/src/App.vue create mode 100644 examples/vue/src/MessageBubble.vue create mode 100644 examples/vue/src/index.ts create mode 100644 examples/vue/src/style.css create mode 100644 examples/vue/src/vite-env.d.ts create mode 100644 examples/vue/vite.config.ts delete mode 100644 packages/vue/src/App.vue create mode 100644 packages/vue/src/index.ts create mode 100644 packages/vue/src/use-chat.ts create mode 100644 tsconfig.vue.json diff --git a/examples/vue/index.html b/examples/vue/index.html new file mode 100644 index 0000000..3313649 --- /dev/null +++ b/examples/vue/index.html @@ -0,0 +1,12 @@ + + + + + + Vue Test + + +
+ + + diff --git a/examples/vue/package.json b/examples/vue/package.json new file mode 100644 index 0000000..b1a6d10 --- /dev/null +++ b/examples/vue/package.json @@ -0,0 +1,19 @@ +{ + "name": "@xsai-use/examples-vue", + "type": "module", + "version": "0.0.1", + "private": true, + "description": "when UI libs meet xsai", + "author": "Moeru AI", + "license": "MIT", + "sideEffects": false, + "scripts": { + "preview": "vite --force" + }, + "dependencies": { + "@xsai-use/vue": "workspace:", + "@xsai/tool": "catalog:xsai", + "valibot": "catalog:valibot", + "vue": "^3.5.13" + } +} diff --git a/examples/vue/src/App.vue b/examples/vue/src/App.vue new file mode 100644 index 0000000..b7927c4 --- /dev/null +++ b/examples/vue/src/App.vue @@ -0,0 +1,186 @@ + + + diff --git a/examples/vue/src/MessageBubble.vue b/examples/vue/src/MessageBubble.vue new file mode 100644 index 0000000..96aac66 --- /dev/null +++ b/examples/vue/src/MessageBubble.vue @@ -0,0 +1,136 @@ + + + diff --git a/examples/vue/src/index.ts b/examples/vue/src/index.ts new file mode 100644 index 0000000..fe5bae3 --- /dev/null +++ b/examples/vue/src/index.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' +import './style.css' + +createApp(App).mount('#app') diff --git a/examples/vue/src/style.css b/examples/vue/src/style.css new file mode 100644 index 0000000..ebbb162 --- /dev/null +++ b/examples/vue/src/style.css @@ -0,0 +1,97 @@ +@import "tailwindcss"; +@plugin "daisyui"; + +.tweak-collapse-title-arrow:after { + top: 1.4rem; +} + +.chat-bubble { + display: flex; + flex-direction: column; + gap: .5rem; +} + +.error-message { + font-size: 12px; +} + +.tweak-collapse { + padding-top: .5rem; + padding-bottom: .5rem; + min-height: 2.75rem; +} + +.useChat-header { + background-color: #f0f2f5; + border-bottom: 1px solid #ddd; + padding: 10px 15px; + text-align: center; +} + +.useChat-container { + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + font-family: Arial, sans-serif; + height: 90vh; + overflow: hidden; + width: 600px; +} + +.input-container { + background-color: #f0f2f5; + border-top: 1px solid #ddd; + display: flex; + padding: 10px; + width: 100%; +} + +.messages-container { + display: flex; + flex: 1; + flex-direction: column; + gap: 10px; + overflow-y: auto; + padding: 15px; +} + +.tools-container { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-start; + gap: 8px; + min-height: 35px; +} + +.tool-badge { + background-color: #e9ecef; + border-radius: 16px; + padding: 6px 12px; + font-size: 13px; + color: #495057; + display: flex; + align-items: center; + gap: 6px; + border: 1px solid #dee2e6; +} + +.tool-icon { + color: #495057; + font-size: 14px; +} + +.tool-name { + font-size: 13px; + color: #495057; +} + +.chat-tools-section { + padding: 10px; + align-content: center; + flex-shrink: 0; + border-bottom: 1px solid #ddd; + background-color: #f8f9fa; +} diff --git a/examples/vue/src/vite-env.d.ts b/examples/vue/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/examples/vue/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/vue/vite.config.ts b/examples/vue/vite.config.ts new file mode 100644 index 0000000..8313cd1 --- /dev/null +++ b/examples/vue/vite.config.ts @@ -0,0 +1,8 @@ +import tailwindcss from '@tailwindcss/vite' +import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [tailwindcss(), vue()], +}) diff --git a/package.json b/package.json index ea5c167..06c0437 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "@testing-library/react": "^16.3.0", "@tsconfig/svelte": "^5.0.4", "@vitejs/plugin-react": "^4.4.1", + "@vitejs/plugin-vue": "^5.2.4", "@vitest/coverage-v8": "catalog:", + "@vue/tsconfig": "^0.7.0", "bumpp": "^10.1.1", "daisyui": "^5.0.35", "eslint": "^9.26.0", diff --git a/packages/vue/package.json b/packages/vue/package.json index 92119b7..5cae44f 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -40,7 +40,7 @@ "coverage": "vitest run --coverage" }, "peerDependencies": { - "vue": "^3.0.0" + "vue": "^3.5.13" }, "dependencies": { "@xsai-use/shared": "workspace:" diff --git a/packages/vue/src/App.vue b/packages/vue/src/App.vue deleted file mode 100644 index 0f31461..0000000 --- a/packages/vue/src/App.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts new file mode 100644 index 0000000..715a3a2 --- /dev/null +++ b/packages/vue/src/index.ts @@ -0,0 +1,2 @@ +export { useChat } from './use-chat' +export type * from '@xsai-use/shared' diff --git a/packages/vue/src/use-chat.ts b/packages/vue/src/use-chat.ts new file mode 100644 index 0000000..4faed59 --- /dev/null +++ b/packages/vue/src/use-chat.ts @@ -0,0 +1,251 @@ +import type { InputMessage, UIMessage, UseChatOptions, UseChatStatus } from '@xsai-use/shared' +import type { MaybeRefOrGetter, Ref } from 'vue' +import { callApi, extractUIMessageParts, generateWeakID } from '@xsai-use/shared' +import { computed, ref, toValue, watch } from 'vue' + +declare global { + interface ReadableStream { + // eslint-disable-next-line ts/method-signature-style + [Symbol.asyncIterator](): AsyncIterableIterator + } +} + +export function useChat(options: MaybeRefOrGetter) { + const getOptions = () => toValue(options) + + const initialUIMessages = computed(() => { + const opts = getOptions() + const messages = opts.initialMessages ?? [] + const idGenerator = opts.generateID ?? generateWeakID + + return messages.map((m) => { + return { + ...m, + id: idGenerator(), + parts: extractUIMessageParts(m), + } + }) + }) + + const messages = ref(initialUIMessages.value) + const status = ref('idle') + const input = ref('') + const error = ref(null) + + let abortController: AbortController | null = null + + watch(initialUIMessages, (newInitialMessages) => { + if (status.value === 'idle') { + messages.value = newInitialMessages + } + }, { deep: true }) + + const request = async ({ messages: requestMessages }: { messages: UIMessage[] }) => { + status.value = 'loading' + error.value = null + + abortController = new AbortController() + + try { + const opts = getOptions() + const { + id: _id, + generateID: _generateID, + initialMessages: _initialMessages, + onFinish: _onFinish, + preventDefault: _preventDefault, + ...currentStreamTextOptions + } = opts + const idGenerator = opts.generateID ?? generateWeakID + + await callApi({ + ...currentStreamTextOptions, + messages: requestMessages, + onFinish: () => { + status.value = 'idle' + abortController = null + // Call the onFinish callback if provided + void opts.onFinish?.() + }, + signal: abortController.signal, + }, { + generateID: idGenerator, + updatingMessage: { + id: idGenerator(), + parts: [], + role: 'assistant', + }, + onUpdate: (message) => { + if (abortController?.signal.aborted) { + return + } + + const clonedMessage = structuredClone(message) + const currentMessages = messages.value + + // Replace the last assistant message or append new one + const newMessages = [ + ...(currentMessages.at(-1)?.role === 'assistant' + ? currentMessages.slice(0, -1) + : currentMessages), + clonedMessage, + ] + + messages.value = newMessages + }, + }) + } + catch (err) { + if (abortController.signal.aborted) { + return + } + status.value = 'error' + const actualError = err instanceof Error ? err : new Error(String(err)) + error.value = actualError + abortController = null + } + } + + const submitMessage = async (message: InputMessage) => { + if (status.value !== 'idle') { + return + } + + // Validate message content + if ( + (Array.isArray(message.content) && message.content.length === 0) + || (typeof message.content === 'string' && message.content.trim() === '') + ) { + return + } + + const opts = getOptions() + const idGenerator = opts.generateID ?? generateWeakID + + const userMessage = { + ...message, + id: idGenerator(), + role: 'user', + } as UIMessage + userMessage.parts = extractUIMessageParts(userMessage) + + const newMessages = [...messages.value, userMessage] + messages.value = newMessages + + await request({ + messages: newMessages, + }) + } + + const handleSubmit = async (e?: Event) => { + const opts = getOptions() + if (opts.preventDefault && e) { + e.preventDefault() + } + + if (!input.value.trim()) { + return + } + + await submitMessage({ + content: [ + { + text: input.value, + type: 'text', + }, + ], + }) + + input.value = '' + } + + const stop = () => { + if (abortController) { + abortController.abort() + abortController = null + status.value = 'idle' + } + } + + const reload = async (id?: string) => { + if (status.value === 'loading') { + return + } + + const currentMessages = messages.value + + if (currentMessages.length === 0) { + return + } + + // Find last user message with matching id (or any user message if id is undefined) + let msgIdx = -1 + for (let i = currentMessages.length - 1; i >= 0; i--) { + if (currentMessages[i].role === 'user' && (id === undefined || currentMessages[i].id === id)) { + msgIdx = i + break + } + } + + // If no matching message found, find the last user message + if (msgIdx === -1) { + for (let i = currentMessages.length - 1; i >= 0; i--) { + if (currentMessages[i].role === 'user') { + msgIdx = i + break + } + } + } + + // Still not found, return + if (msgIdx === -1) { + return + } + + const newMessages = currentMessages.slice(0, msgIdx + 1) + messages.value = newMessages + + await request({ + messages: newMessages, + }) + } + + const reset = () => { + stop() + messages.value = initialUIMessages.value + input.value = '' + error.value = null + status.value = 'idle' + } + + const setMessages = (newMessages: UIMessage[]) => { + if (status.value === 'loading') { + return + } + messages.value = newMessages + } + + const handleInputChange = (e: Event) => { + const target = e.target as HTMLInputElement | HTMLTextAreaElement + input.value = target.value + } + + return { + // state + messages, + status, + input, + error, + + // Actions + submitMessage, + handleSubmit, + handleInputChange, + setMessages, + reload, + reset, + stop, + } +} + +export default useChat diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f0a4ae..7aa3a1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,9 +59,15 @@ importers: '@vitejs/plugin-react': specifier: ^4.4.1 version: 4.4.1(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)) + '@vitejs/plugin-vue': + specifier: ^5.2.4 + version: 5.2.4(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3)) '@vitest/coverage-v8': specifier: 'catalog:' version: 3.1.3(vitest@3.1.3(@types/debug@4.1.12)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(yaml@2.8.0)) + '@vue/tsconfig': + specifier: ^0.7.0 + version: 0.7.0(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)) bumpp: specifier: ^10.1.1 version: 10.1.1(magicast@0.3.5) @@ -151,6 +157,21 @@ importers: specifier: catalog:valibot version: 1.1.0(typescript@5.8.3) + examples/vue: + dependencies: + '@xsai-use/vue': + specifier: 'workspace:' + version: link:../../packages/vue + '@xsai/tool': + specifier: catalog:xsai + version: 0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(@zod/mini@4.0.0-beta.20250505T195954)(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) + valibot: + specifier: catalog:valibot + version: 1.1.0(typescript@5.8.3) + vue: + specifier: ^3.5.13 + version: 3.5.13(typescript@5.8.3) + packages/react: dependencies: '@xsai-use/shared': @@ -1167,6 +1188,13 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + '@vitest/coverage-v8@3.1.3': resolution: {integrity: sha512-cj76U5gXCl3g88KSnf80kof6+6w+K4BjOflCl7t6yRJPDuCrHtVu0SgNYOUARJOL5TI8RScDbm5x4s1/P9bvpw==} peerDependencies: @@ -1246,6 +1274,17 @@ packages: '@vue/shared@3.5.13': resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + '@vue/tsconfig@0.7.0': + resolution: {integrity: sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + '@xsai/shared-chat@0.2.1': resolution: {integrity: sha512-TdeLwS4S77D9UHt1mWDCiEG/9t0HqTzXFvlVAq3tmglX0G1FUn6+2z0wCQa2WqxFFF+GDP319XpI/d6CR8M3aw==} @@ -4451,6 +4490,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-vue@5.2.4(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': + dependencies: + vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0) + vue: 3.5.13(typescript@5.8.3) + '@vitest/coverage-v8@3.1.3(vitest@3.1.3(@types/debug@4.1.12)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(yaml@2.8.0))': dependencies: '@ampproject/remapping': 2.3.0 @@ -4573,6 +4617,11 @@ snapshots: '@vue/shared@3.5.13': {} + '@vue/tsconfig@0.7.0(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))': + optionalDependencies: + typescript: 5.8.3 + vue: 3.5.13(typescript@5.8.3) + '@xsai/shared-chat@0.2.1': dependencies: '@xsai/shared': 0.2.1 diff --git a/tsconfig.json b/tsconfig.json index 15176ac..db0e419 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ { "path": "./tsconfig.node.json" }, { "path": "./tsconfig.react.json" }, { "path": "./tsconfig.svelte.json" }, - { "path": "./examples/svelte/tsconfig.json" } + { "path": "./examples/svelte/tsconfig.json" }, + { "path": "./tsconfig.vue.json" } ], "files": [] } diff --git a/tsconfig.vue.json b/tsconfig.vue.json new file mode 100644 index 0000000..dc91a4f --- /dev/null +++ b/tsconfig.vue.json @@ -0,0 +1,19 @@ +{ + "extends": "@vue/tsconfig/tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "strict": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true, + "noUncheckedSideEffectImports": true + }, + "include": [ + "examples/vue/src", + "packages/vue/src", + "packages/vue/test" + ] +} From ea569196d41f79d69da30055533452f51936d6ce Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:33:21 +0800 Subject: [PATCH 03/18] feat: enhance Vue chat example with improved message handling and new components --- eslint.config.ts | 1 + examples/vue/src/App.vue | 182 +++++++++++++++++++------- examples/vue/src/MessageBubble.vue | 124 ++++-------------- examples/vue/src/MessageParts.vue | 24 ++++ examples/vue/src/MessageToolPart.vue | 55 ++++++++ examples/vue/src/index.css | 2 + examples/vue/src/index.ts | 2 +- examples/vue/src/style.css | 97 -------------- packages/vue/src/use-chat.ts | 82 +++++------- packages/vue/src/utils/deep-to-raw.ts | 27 ++++ 10 files changed, 301 insertions(+), 295 deletions(-) create mode 100644 examples/vue/src/MessageParts.vue create mode 100644 examples/vue/src/MessageToolPart.vue create mode 100644 examples/vue/src/index.css delete mode 100644 examples/vue/src/style.css create mode 100644 packages/vue/src/utils/deep-to-raw.ts diff --git a/eslint.config.ts b/eslint.config.ts index 47cea1e..774d0b6 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -5,6 +5,7 @@ export default antfu( jsx: true, react: true, svelte: true, + vue: true, typescript: { tsconfigPath: './tsconfig.json' }, }, { diff --git a/examples/vue/src/App.vue b/examples/vue/src/App.vue index b7927c4..bf110db 100644 --- a/examples/vue/src/App.vue +++ b/examples/vue/src/App.vue @@ -2,7 +2,7 @@ import { useChat } from '@xsai-use/vue' import { tool } from '@xsai/tool' import { description, object, pipe, string } from 'valibot' -import { ref, onMounted, nextTick, watch } from 'vue' +import { nextTick, onMounted, ref, watch } from 'vue' import MessageBubble from './MessageBubble.vue' @@ -14,9 +14,19 @@ const inputRef = ref(null) const isLoadingTools = ref(true) const loadedTools = ref({}) -// Load tools on component mount +let { + handleSubmit, + handleInputChange, + input, + messages, + status, + error, + reset, + stop, + reload, +} = useChat({}) + onMounted(async () => { - isLoadingTools.value = true // manually delay loading tools to simulate network delay await new Promise(resolve => setTimeout(resolve, 1000)) @@ -38,10 +48,11 @@ onMounted(async () => { parameters: object({ location: pipe( string(), - description('The location to get the weather for') + description('The location to get the weather for'), ), }), }) + loadedTools.value.weather = weatherTool const calculatorTool = await tool({ description: 'Calculate mathematical expression', @@ -53,50 +64,47 @@ onMounted(async () => { parameters: object({ expression: pipe( string(), - description('The mathematical expression to calculate') + description('The mathematical expression to calculate'), ), }), }) + loadedTools.value.calculator = calculatorTool - loadedTools.value = { - weather: weatherTool, - calculator: calculatorTool, - } - } catch (err) { - console.error('Error loading tools:', err) - } finally { isLoadingTools.value = false } + catch (err) { + console.error('Error loading tools:', err) + } + finally { + ; ({ + handleSubmit, + handleInputChange, + input, + messages, + status, + error, + reset, + stop, + reload, + } = useChat({ + id: 'simple-chat', + preventDefault: true, + initialMessages: [ + { + role: 'system', + content: 'you are a helpful assistant.', + }, + ], + baseURL: 'http://localhost:11434/v1/', + model: 'mistral-nemo-instruct-2407', + maxSteps: 3, + toolChoice: 'auto', + tools: Object.values(loadedTools.value), + })) + } }) -const { - handleSubmit, - handleInputChange, - input, - messages, - status, - error, - reset, - stop, - reload, -} = useChat({ - id: 'simple-chat', - preventDefault: true, - initialMessages: [ - { - role: 'system', - content: 'you are a helpful assistant.', - }, - ], - baseURL: 'http://localhost:11434/v1/', - model: 'mistral-nemo-instruct-2407', - maxSteps: 3, - toolChoice: 'auto', - tools: Object.values(loadedTools.value), -}) - -// Handle send button click based on status -const handleSendButtonClick = (e: Event) => { +function handleSendButtonClick(e: Event) { if (status.value === 'loading') { e.preventDefault() stop() @@ -104,7 +112,6 @@ const handleSendButtonClick = (e: Event) => { // Let the form submission handle other cases } -// Focus input when status changes to idle watch(status, (newStatus) => { if (newStatus === 'idle' && inputRef.value) { nextTick(() => { @@ -124,7 +131,7 @@ watch(status, (newStatus) => {
Available tools: - + + + diff --git a/examples/vue/src/MessageBubble.vue b/examples/vue/src/MessageBubble.vue index 96aac66..a2739cb 100644 --- a/examples/vue/src/MessageBubble.vue +++ b/examples/vue/src/MessageBubble.vue @@ -1,8 +1,10 @@ + + diff --git a/examples/vue/src/MessageParts.vue b/examples/vue/src/MessageParts.vue new file mode 100644 index 0000000..7e0c5f9 --- /dev/null +++ b/examples/vue/src/MessageParts.vue @@ -0,0 +1,24 @@ + + + diff --git a/examples/vue/src/MessageToolPart.vue b/examples/vue/src/MessageToolPart.vue new file mode 100644 index 0000000..dd78f2c --- /dev/null +++ b/examples/vue/src/MessageToolPart.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/examples/vue/src/index.css b/examples/vue/src/index.css new file mode 100644 index 0000000..4c1b0c2 --- /dev/null +++ b/examples/vue/src/index.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; +@plugin "daisyui"; diff --git a/examples/vue/src/index.ts b/examples/vue/src/index.ts index fe5bae3..50a4dab 100644 --- a/examples/vue/src/index.ts +++ b/examples/vue/src/index.ts @@ -1,5 +1,5 @@ import { createApp } from 'vue' import App from './App.vue' -import './style.css' +import './index.css' createApp(App).mount('#app') diff --git a/examples/vue/src/style.css b/examples/vue/src/style.css deleted file mode 100644 index ebbb162..0000000 --- a/examples/vue/src/style.css +++ /dev/null @@ -1,97 +0,0 @@ -@import "tailwindcss"; -@plugin "daisyui"; - -.tweak-collapse-title-arrow:after { - top: 1.4rem; -} - -.chat-bubble { - display: flex; - flex-direction: column; - gap: .5rem; -} - -.error-message { - font-size: 12px; -} - -.tweak-collapse { - padding-top: .5rem; - padding-bottom: .5rem; - min-height: 2.75rem; -} - -.useChat-header { - background-color: #f0f2f5; - border-bottom: 1px solid #ddd; - padding: 10px 15px; - text-align: center; -} - -.useChat-container { - border: 1px solid #ddd; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - font-family: Arial, sans-serif; - height: 90vh; - overflow: hidden; - width: 600px; -} - -.input-container { - background-color: #f0f2f5; - border-top: 1px solid #ddd; - display: flex; - padding: 10px; - width: 100%; -} - -.messages-container { - display: flex; - flex: 1; - flex-direction: column; - gap: 10px; - overflow-y: auto; - padding: 15px; -} - -.tools-container { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: flex-start; - gap: 8px; - min-height: 35px; -} - -.tool-badge { - background-color: #e9ecef; - border-radius: 16px; - padding: 6px 12px; - font-size: 13px; - color: #495057; - display: flex; - align-items: center; - gap: 6px; - border: 1px solid #dee2e6; -} - -.tool-icon { - color: #495057; - font-size: 14px; -} - -.tool-name { - font-size: 13px; - color: #495057; -} - -.chat-tools-section { - padding: 10px; - align-content: center; - flex-shrink: 0; - border-bottom: 1px solid #ddd; - background-color: #f8f9fa; -} diff --git a/packages/vue/src/use-chat.ts b/packages/vue/src/use-chat.ts index 4faed59..89e89b3 100644 --- a/packages/vue/src/use-chat.ts +++ b/packages/vue/src/use-chat.ts @@ -1,7 +1,7 @@ import type { InputMessage, UIMessage, UseChatOptions, UseChatStatus } from '@xsai-use/shared' -import type { MaybeRefOrGetter, Ref } from 'vue' import { callApi, extractUIMessageParts, generateWeakID } from '@xsai-use/shared' -import { computed, ref, toValue, watch } from 'vue' +import { computed, readonly, ref, watch } from 'vue' +import { deepToRaw } from './utils/deep-to-raw' declare global { interface ReadableStream { @@ -10,31 +10,33 @@ declare global { } } -export function useChat(options: MaybeRefOrGetter) { - const getOptions = () => toValue(options) - - const initialUIMessages = computed(() => { - const opts = getOptions() - const messages = opts.initialMessages ?? [] - const idGenerator = opts.generateID ?? generateWeakID - - return messages.map((m) => { - return { - ...m, - id: idGenerator(), - parts: extractUIMessageParts(m), - } - }) +export function useChat(options: UseChatOptions) { + const { + id, + generateID = generateWeakID, + initialMessages = [], + onFinish, + preventDefault = false, + ...streamTextOptions + } = options + + const initialUIMessages = initialMessages.map((m) => { + return { + ...m, + id: generateID(), + parts: extractUIMessageParts(m), + } }) - const messages = ref(initialUIMessages.value) + const messages = ref(initialUIMessages) const status = ref('idle') const input = ref('') const error = ref(null) let abortController: AbortController | null = null - watch(initialUIMessages, (newInitialMessages) => { + const initialUIMessagesR = computed(() => initialUIMessages) + watch(initialUIMessagesR, (newInitialMessages) => { if (status.value === 'idle') { messages.value = newInitialMessages } @@ -47,31 +49,19 @@ export function useChat(options: MaybeRefOrGetter) { abortController = new AbortController() try { - const opts = getOptions() - const { - id: _id, - generateID: _generateID, - initialMessages: _initialMessages, - onFinish: _onFinish, - preventDefault: _preventDefault, - ...currentStreamTextOptions - } = opts - const idGenerator = opts.generateID ?? generateWeakID - await callApi({ - ...currentStreamTextOptions, + ...streamTextOptions, messages: requestMessages, onFinish: () => { status.value = 'idle' abortController = null - // Call the onFinish callback if provided - void opts.onFinish?.() + void options.onFinish?.() }, signal: abortController.signal, }, { - generateID: idGenerator, + generateID, updatingMessage: { - id: idGenerator(), + id: generateID(), parts: [], role: 'assistant', }, @@ -119,12 +109,9 @@ export function useChat(options: MaybeRefOrGetter) { return } - const opts = getOptions() - const idGenerator = opts.generateID ?? generateWeakID - const userMessage = { ...message, - id: idGenerator(), + id: generateID(), role: 'user', } as UIMessage userMessage.parts = extractUIMessageParts(userMessage) @@ -133,15 +120,12 @@ export function useChat(options: MaybeRefOrGetter) { messages.value = newMessages await request({ - messages: newMessages, + messages: deepToRaw(newMessages), }) } const handleSubmit = async (e?: Event) => { - const opts = getOptions() - if (opts.preventDefault && e) { - e.preventDefault() - } + preventDefault && e?.preventDefault?.() if (!input.value.trim()) { return @@ -206,13 +190,13 @@ export function useChat(options: MaybeRefOrGetter) { messages.value = newMessages await request({ - messages: newMessages, + messages: deepToRaw(newMessages), }) } const reset = () => { stop() - messages.value = initialUIMessages.value + messages.value = initialUIMessages input.value = '' error.value = null status.value = 'idle' @@ -232,10 +216,10 @@ export function useChat(options: MaybeRefOrGetter) { return { // state - messages, - status, + messages: readonly(messages), + status: readonly(status), input, - error, + error: readonly(error), // Actions submitMessage, diff --git a/packages/vue/src/utils/deep-to-raw.ts b/packages/vue/src/utils/deep-to-raw.ts new file mode 100644 index 0000000..d28c1d9 --- /dev/null +++ b/packages/vue/src/utils/deep-to-raw.ts @@ -0,0 +1,27 @@ +import { + isProxy, + isReactive, + isRef, + toRaw, +} from 'vue' + +export function deepToRaw(input: T): T { + if (Array.isArray(input)) { + return [...input.map(deepToRaw) as unknown[]] as T + } + + if (isRef(input) + || isReactive(input) + || isProxy(input)) { + return deepToRaw(toRaw(input)) + } + + if (input !== null && typeof input === 'object') { + const clone = {} + for (const [key, value] of Object.entries(input)) { + (clone as Record)[key] = deepToRaw(value) + } + return clone as T + } + return input +} From 4d1152f763343d88fa9656eab2c1969881a71649 Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:33:21 +0800 Subject: [PATCH 04/18] feat: implement chat functionality with tool integration and UI components --- examples/vue/src/App.vue | 267 +------------------------------------ examples/vue/src/Chat.vue | 270 ++++++++++++++++++++++++++++++++++++++ tsconfig.vue.json | 5 +- 3 files changed, 273 insertions(+), 269 deletions(-) create mode 100644 examples/vue/src/Chat.vue diff --git a/examples/vue/src/App.vue b/examples/vue/src/App.vue index bf110db..c5c0086 100644 --- a/examples/vue/src/App.vue +++ b/examples/vue/src/App.vue @@ -1,270 +1,7 @@ - - diff --git a/examples/vue/src/Chat.vue b/examples/vue/src/Chat.vue new file mode 100644 index 0000000..bf110db --- /dev/null +++ b/examples/vue/src/Chat.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/tsconfig.vue.json b/tsconfig.vue.json index dc91a4f..4ee4a63 100644 --- a/tsconfig.vue.json +++ b/tsconfig.vue.json @@ -1,13 +1,10 @@ { - "extends": "@vue/tsconfig/tsconfig.json", + "extends": "@vue/tsconfig/tsconfig.dom.json", "compilerOptions": { - "jsx": "preserve", - "jsxImportSource": "vue", "strict": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, - "verbatimModuleSyntax": true, "erasableSyntaxOnly": true, "noUncheckedSideEffectImports": true }, From cfdaace1533e05fa18b3428416633dc86a374317 Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 05/18] feat: add vue-tsc as a devDependency in package.json and update pnpm-lock.yaml --- examples/vue/package.json | 3 + pnpm-lock.yaml | 163 +++++++++++++++++++++++++++++--------- 2 files changed, 128 insertions(+), 38 deletions(-) diff --git a/examples/vue/package.json b/examples/vue/package.json index b1a6d10..f8c5163 100644 --- a/examples/vue/package.json +++ b/examples/vue/package.json @@ -15,5 +15,8 @@ "@xsai/tool": "catalog:xsai", "valibot": "catalog:valibot", "vue": "^3.5.13" + }, + "devDependencies": { + "vue-tsc": "^2.2.10" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aa3a1c..6d8b533 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,7 +23,7 @@ catalogs: xsai: '@xsai/shared-chat': specifier: ^0.2.1 - version: 0.2.1 + version: 0.2.2 '@xsai/stream-text': specifier: ^0.2.1 version: 0.2.1 @@ -118,7 +118,7 @@ importers: version: link:../../packages/react '@xsai/tool': specifier: catalog:xsai - version: 0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(@zod/mini@4.0.0-beta.20250505T195954)(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) + version: 0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) classnames: specifier: ^2.5.1 version: 2.5.1 @@ -149,7 +149,7 @@ importers: version: link:../../packages/svelte '@xsai/tool': specifier: catalog:xsai - version: 0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(@zod/mini@4.0.0-beta.20250505T195954)(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) + version: 0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) svelte: specifier: ^5.33.19 version: 5.33.19 @@ -164,13 +164,17 @@ importers: version: link:../../packages/vue '@xsai/tool': specifier: catalog:xsai - version: 0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(@zod/mini@4.0.0-beta.20250505T195954)(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) + version: 0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) valibot: specifier: catalog:valibot version: 1.1.0(typescript@5.8.3) vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.8.3) + devDependencies: + vue-tsc: + specifier: ^2.2.10 + version: 2.2.10(typescript@5.8.3) packages/react: dependencies: @@ -189,7 +193,7 @@ importers: version: 1.1.0(valibot@1.1.0(typescript@5.8.3)) '@xsai/tool': specifier: catalog:xsai - version: 0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(@zod/mini@4.0.0-beta.20250505T195954)(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) + version: 0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) react: specifier: ^19.1.0 version: 19.1.0 @@ -204,7 +208,7 @@ importers: dependencies: '@xsai/shared-chat': specifier: catalog:xsai - version: 0.2.1 + version: 0.2.2 '@xsai/stream-text': specifier: catalog:xsai version: 0.2.1 @@ -1245,6 +1249,15 @@ packages: '@vitest/utils@3.1.3': resolution: {integrity: sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==} + '@volar/language-core@2.4.14': + resolution: {integrity: sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==} + + '@volar/source-map@2.4.14': + resolution: {integrity: sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==} + + '@volar/typescript@2.4.14': + resolution: {integrity: sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==} + '@vue/compiler-core@3.5.13': resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} @@ -1257,6 +1270,17 @@ packages: '@vue/compiler-ssr@3.5.13': resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.10': + resolution: {integrity: sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@vue/reactivity@3.5.13': resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} @@ -1285,17 +1309,14 @@ packages: vue: optional: true - '@xsai/shared-chat@0.2.1': - resolution: {integrity: sha512-TdeLwS4S77D9UHt1mWDCiEG/9t0HqTzXFvlVAq3tmglX0G1FUn6+2z0wCQa2WqxFFF+GDP319XpI/d6CR8M3aw==} - '@xsai/shared-chat@0.2.2': resolution: {integrity: sha512-0t4HwbK2jm5Lrixg9RnIsYTtAvlQNUdJXs2taW7PFaXfL5ktMy2IwZwW2nqK+Ig0RcLCMp3sScZujERHSDMtmg==} - '@xsai/shared@0.2.1': - resolution: {integrity: sha512-ddxVkwWoz2sgic8KbVSWp/1pmt3HDsSLcnAWxmI2bXvl2CHwumtMH+VqXPkLlFBdDXOpsn4T0EnoZXJeNl5uVQ==} + '@xsai/shared-chat@0.3.0-beta.4': + resolution: {integrity: sha512-7vD3XLl5I3RKntFGJTPRe5fGner+49KwRj7MQAUSjykvQGtJ6CoEGcZcQdhnqrsRZga8b2urpiMRZZCHL4avzg==} - '@xsai/shared@0.2.2': - resolution: {integrity: sha512-bgalLd3samx36hDRRmu3M75LY5NfWwh3YRaMwY4Woy7X09JQx+vEqVpJcaIeqcmVqztm8NGYzj8hUru7lwPh0w==} + '@xsai/shared@0.3.0-beta.4': + resolution: {integrity: sha512-Jf/AMvk/MAZu1prsr67mCXwfjjXyDaKgkL/a/3oJ1PKub0VLrtu7W7J8ZE1YOTqPhfiuSjaDxKMoUJe+BY++QA==} '@xsai/stream-text@0.2.1': resolution: {integrity: sha512-kupWhArdIc+7uM4d3X8ku3PB7PuG1eLVk67WqXT9YNbMo+7ICUbsX4nWojVkpbVT9vnmy8ZG+gklyExSBZn2Ag==} @@ -1330,6 +1351,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1578,6 +1602,9 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -2170,6 +2197,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -2655,6 +2686,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2745,6 +2779,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3345,12 +3382,21 @@ packages: jsdom: optional: true + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-eslint-parser@10.1.3: resolution: {integrity: sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + vue-tsc@2.2.10: + resolution: {integrity: sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + vue@3.5.13: resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} peerDependencies: @@ -3427,21 +3473,18 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xsschema@0.2.2: - resolution: {integrity: sha512-lRKF/5sqN+Yv2v8UdzjddrfeM5UAOR98tIgFkwXODxMwco4ImCmM1JBhfWaDukPeWsU1eHmOta8AsNAXiJZ1Cw==} + xsschema@0.3.0-beta.4: + resolution: {integrity: sha512-XROJ0QTt5p/awHveolz+gM+LP5lj51dh0oO8G28zvpwpevm4oPSHkRH8nOLKYlLgZ/Vk/mZptY5n+yDyPiNgXg==} peerDependencies: '@valibot/to-json-schema': ^1.0.0 - '@zod/mini': ^4.0.0-beta arktype: ^2.1.16 effect: ^3.14.5 sury: ^10.0.0-rc - zod: ^3.24.3 || ^4.0.0-beta + zod: ^3.25.0 zod-to-json-schema: ^3.24.5 peerDependenciesMeta: '@valibot/to-json-schema': optional: true - '@zod/mini': - optional: true arktype: optional: true effect: @@ -4499,7 +4542,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.0 + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -4563,6 +4606,18 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 + '@volar/language-core@2.4.14': + dependencies: + '@volar/source-map': 2.4.14 + + '@volar/source-map@2.4.14': {} + + '@volar/typescript@2.4.14': + dependencies: + '@volar/language-core': 2.4.14 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + '@vue/compiler-core@3.5.13': dependencies: '@babel/parser': 7.27.2 @@ -4593,6 +4648,24 @@ snapshots: '@vue/compiler-dom': 3.5.13 '@vue/shared': 3.5.13 + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.10(typescript@5.8.3)': + dependencies: + '@volar/language-core': 2.4.14 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.13 + alien-signals: 1.0.13 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.8.3 + '@vue/reactivity@3.5.13': dependencies: '@vue/shared': 3.5.13 @@ -4622,30 +4695,27 @@ snapshots: typescript: 5.8.3 vue: 3.5.13(typescript@5.8.3) - '@xsai/shared-chat@0.2.1': - dependencies: - '@xsai/shared': 0.2.1 - '@xsai/shared-chat@0.2.2': dependencies: - '@xsai/shared': 0.2.2 + '@xsai/shared': 0.3.0-beta.4 - '@xsai/shared@0.2.1': {} + '@xsai/shared-chat@0.3.0-beta.4': + dependencies: + '@xsai/shared': 0.3.0-beta.4 - '@xsai/shared@0.2.2': {} + '@xsai/shared@0.3.0-beta.4': {} '@xsai/stream-text@0.2.1': dependencies: - '@xsai/shared-chat': 0.2.1 + '@xsai/shared-chat': 0.3.0-beta.4 - '@xsai/tool@0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(@zod/mini@4.0.0-beta.20250505T195954)(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4)': + '@xsai/tool@0.2.1(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4)': dependencies: - '@xsai/shared': 0.2.2 - '@xsai/shared-chat': 0.2.2 - xsschema: 0.2.2(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(@zod/mini@4.0.0-beta.20250505T195954)(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) + '@xsai/shared': 0.3.0-beta.4 + '@xsai/shared-chat': 0.3.0-beta.4 + xsschema: 0.3.0-beta.4(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4) transitivePeerDependencies: - '@valibot/to-json-schema' - - '@zod/mini' - arktype - effect - sury @@ -4678,6 +4748,8 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + alien-signals@1.0.13: {} + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -4907,6 +4979,8 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + de-indent@1.0.2: {} + debug@4.4.0: dependencies: ms: 2.1.3 @@ -5647,6 +5721,8 @@ snapshots: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -5751,7 +5827,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.0 + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -6290,6 +6366,8 @@ snapshots: ms@2.1.3: {} + muggle-string@0.4.1: {} + nanoid@3.3.11: {} natural-compare@1.4.0: {} @@ -6369,6 +6447,8 @@ snapshots: parseurl@1.3.3: {} + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -6924,7 +7004,7 @@ snapshots: vite-node@3.1.3(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0): dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 6.3.5(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0) @@ -6970,7 +7050,7 @@ snapshots: '@vitest/spy': 3.1.3 '@vitest/utils': 3.1.3 chai: 5.2.0 - debug: 4.4.0 + debug: 4.4.1 expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 @@ -7000,6 +7080,8 @@ snapshots: - tsx - yaml + vscode-uri@3.1.0: {} + vue-eslint-parser@10.1.3(eslint@9.26.0(jiti@2.4.2)): dependencies: debug: 4.4.0 @@ -7013,6 +7095,12 @@ snapshots: transitivePeerDependencies: - supports-color + vue-tsc@2.2.10(typescript@5.8.3): + dependencies: + '@volar/typescript': 2.4.14 + '@vue/language-core': 2.2.10(typescript@5.8.3) + typescript: 5.8.3 + vue@3.5.13(typescript@5.8.3): dependencies: '@vue/compiler-dom': 3.5.13 @@ -7073,10 +7161,9 @@ snapshots: xmlchars@2.2.0: {} - xsschema@0.2.2(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(@zod/mini@4.0.0-beta.20250505T195954)(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4): + xsschema@0.3.0-beta.4(@valibot/to-json-schema@1.1.0(valibot@1.1.0(typescript@5.8.3)))(zod-to-json-schema@3.24.5(zod@3.24.4))(zod@3.24.4): optionalDependencies: '@valibot/to-json-schema': 1.1.0(valibot@1.1.0(typescript@5.8.3)) - '@zod/mini': 4.0.0-beta.20250505T195954 zod: 3.24.4 zod-to-json-schema: 3.24.5(zod@3.24.4) From 5b24b027e98f53a5254ebca549a5f6db041f8109 Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 06/18] fix: cast App as Component in createApp for type safety --- examples/vue/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/vue/src/index.ts b/examples/vue/src/index.ts index 50a4dab..3872ba6 100644 --- a/examples/vue/src/index.ts +++ b/examples/vue/src/index.ts @@ -1,5 +1,6 @@ +import type { Component } from 'vue' import { createApp } from 'vue' import App from './App.vue' import './index.css' -createApp(App).mount('#app') +createApp(App as Component).mount('#app') From 33859008857495acd527049a78de6ed7852003f5 Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 07/18] Update examples/vue/src/Chat.vue Co-authored-by: Neko --- examples/vue/src/Chat.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/vue/src/Chat.vue b/examples/vue/src/Chat.vue index bf110db..85b2162 100644 --- a/examples/vue/src/Chat.vue +++ b/examples/vue/src/Chat.vue @@ -10,7 +10,7 @@ interface ToolMap { [key: string]: Awaited> } -const inputRef = ref(null) +const inputRef = ref() const isLoadingTools = ref(true) const loadedTools = ref({}) From 1adf78e28f860a8b20af589e89dba3a6dd25b93a Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 08/18] Update examples/vue/src/MessageBubble.vue Co-authored-by: Neko --- examples/vue/src/MessageBubble.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/vue/src/MessageBubble.vue b/examples/vue/src/MessageBubble.vue index a2739cb..7ac10c1 100644 --- a/examples/vue/src/MessageBubble.vue +++ b/examples/vue/src/MessageBubble.vue @@ -6,7 +6,7 @@ import MessageParts from './MessageParts.vue' interface Props { message: DeepReadonly isError?: boolean - error?: Error | null + error?: Error reload?: (id: string) => void | Promise } From 9064eedb827ea61575dfdea3cd3f33d1b7a9513c Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 09/18] Update examples/vue/src/MessageBubble.vue Co-authored-by: Neko --- examples/vue/src/MessageBubble.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/vue/src/MessageBubble.vue b/examples/vue/src/MessageBubble.vue index 7ac10c1..4220862 100644 --- a/examples/vue/src/MessageBubble.vue +++ b/examples/vue/src/MessageBubble.vue @@ -12,7 +12,6 @@ interface Props { const props = withDefaults(defineProps(), { isError: false, - error: null, }) From 45077de74db4fbb2fb0776fd9eee2ebcb085e1d5 Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 10/18] Update examples/vue/src/Chat.vue Co-authored-by: Neko --- examples/vue/src/Chat.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/vue/src/Chat.vue b/examples/vue/src/Chat.vue index 85b2162..e9433e7 100644 --- a/examples/vue/src/Chat.vue +++ b/examples/vue/src/Chat.vue @@ -147,11 +147,11 @@ watch(status, (newStatus) => {
From e760b9040102abc7b3bae39627e758e9b86d80ba Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 11/18] Update packages/vue/src/use-chat.ts Co-authored-by: Neko --- packages/vue/src/use-chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue/src/use-chat.ts b/packages/vue/src/use-chat.ts index 89e89b3..4ec608b 100644 --- a/packages/vue/src/use-chat.ts +++ b/packages/vue/src/use-chat.ts @@ -31,7 +31,7 @@ export function useChat(options: UseChatOptions) { const messages = ref(initialUIMessages) const status = ref('idle') const input = ref('') - const error = ref(null) + const error = ref() let abortController: AbortController | null = null From d24aeaa70657f864e5330fadb40d7fbb8170db70 Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 12/18] Update packages/vue/src/use-chat.ts Co-authored-by: Neko --- packages/vue/src/use-chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue/src/use-chat.ts b/packages/vue/src/use-chat.ts index 4ec608b..5ffa7b1 100644 --- a/packages/vue/src/use-chat.ts +++ b/packages/vue/src/use-chat.ts @@ -44,7 +44,7 @@ export function useChat(options: UseChatOptions) { const request = async ({ messages: requestMessages }: { messages: UIMessage[] }) => { status.value = 'loading' - error.value = null + error.value = undefined abortController = new AbortController() From 7b36944b123bbf85324d55da3a631d9c08afc763 Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 13/18] Update packages/vue/src/use-chat.ts Co-authored-by: Neko --- packages/vue/src/use-chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue/src/use-chat.ts b/packages/vue/src/use-chat.ts index 5ffa7b1..ecfa98f 100644 --- a/packages/vue/src/use-chat.ts +++ b/packages/vue/src/use-chat.ts @@ -146,7 +146,7 @@ export function useChat(options: UseChatOptions) { const stop = () => { if (abortController) { abortController.abort() - abortController = null + abortController = undefined status.value = 'idle' } } From 57405ba803dc1d48d7f31df1a68e8958f319215d Mon Sep 17 00:00:00 2001 From: exoticknight Date: Sat, 14 Jun 2025 17:34:13 +0800 Subject: [PATCH 14/18] Fix result rendering in MessageToolPart and optimize deepToRaw function --- examples/vue/src/MessageToolPart.vue | 2 +- packages/vue/src/utils/deep-to-raw.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/vue/src/MessageToolPart.vue b/examples/vue/src/MessageToolPart.vue index dd78f2c..2914295 100644 --- a/examples/vue/src/MessageToolPart.vue +++ b/examples/vue/src/MessageToolPart.vue @@ -27,7 +27,7 @@ const isLoading = computed(() => props.part.status === 'loading' || props.part.s
- {{ String(item) }} + {{ item.text }}
Tool Result