Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e3e0e54
chore: update vue version to ^3.5.13
exoticknight Jun 14, 2025
81d4815
feat: add Vue example with chat functionality and update dependencies
exoticknight Jun 14, 2025
ea56919
feat: enhance Vue chat example with improved message handling and new…
exoticknight Jun 14, 2025
4d1152f
feat: implement chat functionality with tool integration and UI compo…
exoticknight Jun 14, 2025
cfdaace
feat: add vue-tsc as a devDependency in package.json and update pnpm-…
exoticknight Jun 14, 2025
5b24b02
fix: cast App as Component in createApp for type safety
exoticknight Jun 14, 2025
3385900
Update examples/vue/src/Chat.vue
exoticknight Jun 14, 2025
1adf78e
Update examples/vue/src/MessageBubble.vue
exoticknight Jun 14, 2025
9064eed
Update examples/vue/src/MessageBubble.vue
exoticknight Jun 14, 2025
45077de
Update examples/vue/src/Chat.vue
exoticknight Jun 14, 2025
e760b90
Update packages/vue/src/use-chat.ts
exoticknight Jun 14, 2025
d24aeaa
Update packages/vue/src/use-chat.ts
exoticknight Jun 14, 2025
7b36944
Update packages/vue/src/use-chat.ts
exoticknight Jun 14, 2025
57405ba
Fix result rendering in MessageToolPart and optimize deepToRaw function
exoticknight Jun 14, 2025
e822275
Reduce simulated network delay in Chat.vue and update abortController…
exoticknight Jun 14, 2025
6cd5129
Update packages/vue/src/utils/deep-to-raw.ts
exoticknight Jun 14, 2025
bff9cb4
Refactor deepToRaw function for improved readability and maintainability
exoticknight Jun 14, 2025
755ec23
Update packages/vue/src/utils/deep-to-raw.ts
exoticknight Jun 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default antfu(
jsx: true,
react: true,
svelte: true,
vue: true,
typescript: { tsconfigPath: './tsconfig.json' },
},
{
Expand Down
12 changes: 12 additions & 0 deletions examples/vue/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue Test</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/index.ts"></script>
</body>
</html>
22 changes: 22 additions & 0 deletions examples/vue/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"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"
},
"devDependencies": {
"vue-tsc": "^2.2.10"
}
}
7 changes: 7 additions & 0 deletions examples/vue/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup lang="ts">
import Chat from './Chat.vue'
</script>

<template>
<Chat />
</template>
270 changes: 270 additions & 0 deletions examples/vue/src/Chat.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
<script setup lang="ts">
import { useChat } from '@xsai-use/vue'
import { tool } from '@xsai/tool'
import { description, object, pipe, string } from 'valibot'
import { nextTick, onMounted, ref, watch } from 'vue'

import MessageBubble from './MessageBubble.vue'

interface ToolMap {
[key: string]: Awaited<ReturnType<typeof tool>>
}

const inputRef = ref<HTMLInputElement>()
const isLoadingTools = ref(true)
const loadedTools = ref<ToolMap>({})

let {
handleSubmit,
handleInputChange,
input,
messages,
status,
error,
reset,
stop,
reload,
} = useChat({})

onMounted(async () => {
// manually delay loading tools to simulate network delay
await new Promise(resolve => setTimeout(resolve, 1000))

try {
const weatherTool = await tool({
description: 'Get the weather in a location',
execute: async ({ location }) => {
// manually delay loading tools to simulate network delay
await new Promise(resolve => setTimeout(resolve, 1000))
if (Math.random() > 0.5) {
throw new Error('Weather API error')
}
return {
location,
temperature: 10,
}
},
name: 'weather',
parameters: object({
location: pipe(
string(),
description('The location to get the weather for'),
),
}),
})
loadedTools.value.weather = weatherTool

const calculatorTool = await tool({
description: 'Calculate mathematical expression',
execute: ({ expression }) => ({
// eslint-disable-next-line no-eval
result: eval(expression),
}),
name: 'calculator',
parameters: object({
expression: pipe(
string(),
description('The mathematical expression to calculate'),
),
}),
})
loadedTools.value.calculator = calculatorTool

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),
}))
}
})

function handleSendButtonClick(e: Event) {
if (status.value === 'loading') {
e.preventDefault()
stop()
}
// Let the form submission handle other cases
}

watch(status, (newStatus) => {
if (newStatus === 'idle' && inputRef.value) {
nextTick(() => {
inputRef.value?.focus()
})
}
})
</script>

<template>
<div style="display: flex; justify-content: center; padding: 20px;">
<div class="useChat-container">
<div class="useChat-header">
<h2>useChat</h2>
</div>

<div class="chat-tools-section">
<div class="tools-container">
<span>Available tools:</span>
<span v-if="isLoadingTools" class="loading loading-infinity loading-md" />
<template v-else>
<div
v-for="toolName in Object.keys(loadedTools)"
:key="toolName"
class="tool-badge"
>
<span class="tool-icon">🔧</span>
<span class="tool-name">{{ toolName }}</span>
</div>
</template>
</div>
</div>

<div class="messages-container">
<MessageBubble
v-for="(message, index) in messages"
:key="message?.id || index"
:message="message"
:is-error="index === messages.length - 1 && status === 'error'"
:error="index === messages.length - 1 ? error : undefined"
:reload="reload"
/>
</div>

<form class="input-container" @submit="handleSubmit">
<div class="join" style="width: 100%;">
<input
ref="inputRef"
type="text"
class="input join-item"
placeholder="say something..."
style="width: 100%;"
:value="input"
:disabled="status !== 'idle'"
@input="handleInputChange"
>
<button
class="btn join-item"
:type="status === 'loading' ? 'button' : 'submit'"
@click="handleSendButtonClick"
>
{{ status === 'loading' ? 'Stop' : 'Send' }}
</button>
<button
class="btn join-item"
type="button"
@click="(e) => {
e.preventDefault()
reset()
}"
>
<span v-if="status === 'loading'" class="loading loading-dots loading-md" />
<span v-else>Reset</span>
</button>
</div>
</form>
</div>
</div>
</template>

<style>
.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;
}
</style>
61 changes: 61 additions & 0 deletions examples/vue/src/MessageBubble.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup lang="ts">
import type { UIMessage } from '@xsai-use/vue'
import type { DeepReadonly } from 'vue'
import MessageParts from './MessageParts.vue'

interface Props {
message: DeepReadonly<UIMessage>
isError?: boolean
error?: Error
reload?: (id: string) => void | Promise<void>
}

const props = withDefaults(defineProps<Props>(), {
isError: false,
})
</script>

<template>
<div v-if="props.message.role === 'system'" class="flex justify-center">
<div class="badge badge-ghost">
<MessageParts :parts="props.message.parts" />
</div>
</div>

<div v-else class="chat" :class="[props.message.role === 'user' ? 'chat-end' : 'chat-start']">
<div
class="chat-bubble"
:class="[
props.message.role === 'user' ? 'chat-bubble-primary' : '',
]"
>
<MessageParts :parts="props.message.parts" />

<div v-if="props.isError" class="error-message">
❌ {{ props.error?.message }}
</div>
</div>

<div v-if="props.message.role === 'user'" class="chat-footer opacity-50">
<button
type="button"
class="link"
@click="props.reload?.(props.message.id)"
>
reload from here
</button>
</div>
</div>
</template>

<style>
.chat-bubble {
display: flex;
flex-direction: column;
gap: .5rem;
}

.error-message {
font-size: 12px;
}
</style>
Loading