Skip to content

Commit 6f9ef3f

Browse files
feat: follow-up changes from the nuxt version (#14)
Co-authored-by: Benjamin Canac <[email protected]>
1 parent a693d44 commit 6f9ef3f

File tree

14 files changed

+1279
-106
lines changed

14 files changed

+1279
-106
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@
1515
"@ai-sdk/gateway": "^2.0.2",
1616
"@iconify/vue": "^5.0.0",
1717
"@nuxt/ui": "^4.1.0",
18+
"@unovis/vue": "^1.6.1",
1819
"ai": "^5.0.80",
1920
"date-fns": "^4.1.0",
2021
"defu": "^6.1.4",
2122
"drizzle-orm": "^0.44.7",
2223
"katex": "^0.16.25",
2324
"mermaid": "^11.12.0",
24-
"nitro": "npm:nitro-nightly@3.1.0-20251028-090722-437659e4",
25+
"nitro": "npm:nitro-nightly@3.0.1-20251030-220619-920c05a8",
2526
"ofetch": "^1.4.1",
2627
"pg": "^8.16.3",
2728
"shiki-stream": "^0.1.2",
2829
"ufo": "^1.6.1",
2930
"vue": "^3.5.22",
31+
"vue-chrts": "^1.0.2",
3032
"vue-renderer-markdown": "0.0.59",
3133
"vue-router": "^4.6.3",
3234
"vue-use-monaco": "^0.0.33",

pnpm-lock.yaml

Lines changed: 745 additions & 64 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/routes/api/chats/[id].get.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export default defineEventHandler(async (event) => {
1414
const chat = await useDrizzle().query.chats.findFirst({
1515
where: (chat, { eq }) => and(eq(chat.id, id as string), eq(chat.userId, session.data.user?.id || session.id!)),
1616
with: {
17-
messages: true
17+
messages: {
18+
orderBy: (message, { asc }) => asc(message.createdAt)
19+
}
1820
}
1921
})
2022

server/routes/api/chats/[id].post.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, generateText, streamText } from 'ai'
1+
import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, generateText, smoothStream, stepCountIs, streamText } from 'ai'
22
import { gateway } from '@ai-sdk/gateway'
33
import type { UIMessage } from 'ai'
44
import { z } from 'zod'
55
import { useUserSession } from '../../../utils/session'
66
import { useDrizzle, tables, eq, and } from '../../../utils/drizzle'
77
import { defineEventHandler, getValidatedRouterParams, readValidatedBody, HTTPError } from 'nitro/deps/h3'
8+
import { weatherTool } from '../../../utils/tools/weather'
9+
import { chartTool } from '../../../utils/tools/chart'
810

911
export default defineEventHandler(async (event) => {
1012
const session = await useUserSession(event)
@@ -58,8 +60,41 @@ export default defineEventHandler(async (event) => {
5860
execute: ({ writer }) => {
5961
const result = streamText({
6062
model: gateway(model),
61-
system: 'You are a helpful assistant that can answer questions and help.',
62-
messages: convertToModelMessages(messages)
63+
system: `You are a knowledgeable and helpful AI assistant. ${session.data.user?.username ? `The user's name is ${session.data.user.username}.` : ''} Your goal is to provide clear, accurate, and well-structured responses.
64+
65+
**FORMATTING RULES (CRITICAL):**
66+
- ABSOLUTELY NO MARKDOWN HEADINGS: Never use #, ##, ###, ####, #####, or ######
67+
- NO underline-style headings with === or ---
68+
- Use **bold text** for emphasis and section labels instead
69+
- Examples:
70+
* Instead of "## Usage", write "**Usage:**" or just "Here's how to use it:"
71+
* Instead of "# Complete Guide", write "**Complete Guide**" or start directly with content
72+
- Start all responses with content, never with a heading
73+
74+
**RESPONSE QUALITY:**
75+
- Be concise yet comprehensive
76+
- Use examples when helpful
77+
- Break down complex topics into digestible parts
78+
- Maintain a friendly, professional tone`,
79+
messages: convertToModelMessages(messages),
80+
providerOptions: {
81+
openai: {
82+
reasoningEffort: 'low',
83+
reasoningSummary: 'detailed'
84+
},
85+
google: {
86+
thinkingConfig: {
87+
includeThoughts: true,
88+
thinkingBudget: 2048
89+
}
90+
}
91+
},
92+
stopWhen: stepCountIs(5),
93+
experimental_transform: smoothStream({ chunking: 'word' }),
94+
tools: {
95+
weather: weatherTool,
96+
chart: chartTool
97+
}
6398
})
6499

65100
if (!chat.title) {
@@ -70,7 +105,9 @@ export default defineEventHandler(async (event) => {
70105
})
71106
}
72107

73-
writer.merge(result.toUIMessageStream())
108+
writer.merge(result.toUIMessageStream({
109+
sendReasoning: true
110+
}))
74111
},
75112
onFinish: async ({ messages }) => {
76113
await db.insert(tables.messages).values(messages.map(message => ({

server/utils/tools/chart.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { tool } from 'ai'
2+
import { z } from 'zod'
3+
import type { UIToolInvocation } from 'ai'
4+
5+
export type ChartUIToolInvocation = UIToolInvocation<typeof chartTool>
6+
7+
export const chartTool = tool({
8+
description: 'Create a line chart visualization with one or multiple data series. Use this tool to display time-series data, trends, or comparisons between different metrics over time.',
9+
inputSchema: z.object({
10+
title: z.string().optional().describe('Title of the chart'),
11+
data: z.array(z.record(z.string(), z.union([z.string(), z.number()]))).min(1).describe('REQUIRED: Array of data points (minimum 1 point). Each object must contain the xKey property and all series keys'),
12+
xKey: z.string().describe('The property name in data objects to use for x-axis values (e.g., "month", "date")'),
13+
series: z.array(z.object({
14+
key: z.string().describe('The property name in data objects for this series (must exist in all data points)'),
15+
name: z.string().describe('Display name for this series in the legend'),
16+
color: z.string().describe('Hex color code for this line (e.g., "#3b82f6" for blue, "#10b981" for green)')
17+
})).min(1).describe('Array of series configurations (minimum 1 series). Each series represents one line on the chart'),
18+
xLabel: z.string().optional().describe('Optional label for x-axis'),
19+
yLabel: z.string().optional().describe('Optional label for y-axis')
20+
}),
21+
execute: async ({ title, data, xKey, series, xLabel, yLabel }) => {
22+
// Create a delay to simulate the input-available state
23+
await new Promise(resolve => setTimeout(resolve, 1500))
24+
25+
return {
26+
title,
27+
data,
28+
xKey,
29+
series,
30+
xLabel,
31+
yLabel
32+
}
33+
}
34+
})

server/utils/tools/weather.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { UIToolInvocation } from 'ai'
2+
import { tool } from 'ai'
3+
import { z } from 'zod'
4+
5+
export type WeatherUIToolInvocation = UIToolInvocation<typeof weatherTool>
6+
7+
const getWeatherData = (k: string) => ({
8+
'sunny': { text: 'Sunny', icon: 'i-lucide-sun' },
9+
'partly-cloudy': { text: 'Partly Cloudy', icon: 'i-lucide-cloud-sun' },
10+
'cloudy': { text: 'Cloudy', icon: 'i-lucide-cloud' },
11+
'rainy': { text: 'Rainy', icon: 'i-lucide-cloud-rain' },
12+
'foggy': { text: 'Foggy', icon: 'i-lucide-cloud-fog' }
13+
}[k] || { text: 'Sunny', icon: 'i-lucide-sun' })
14+
15+
export const weatherTool = tool({
16+
description: 'Get weather info with 5-day forecast',
17+
inputSchema: z.object({ location: z.string().describe('Location for weather') }),
18+
execute: async ({ location }) => {
19+
// Create a delay to simulate the input-available state
20+
await new Promise(resolve => setTimeout(resolve, 1500))
21+
22+
const temp = Math.floor(Math.random() * 35) + 5
23+
const conds = ['sunny', 'partly-cloudy', 'cloudy', 'rainy', 'foggy'] as const
24+
return {
25+
location,
26+
temperature: Math.round(temp),
27+
temperatureHigh: Math.round(temp + Math.random() * 5 + 2),
28+
temperatureLow: Math.round(temp - Math.random() * 5 - 2),
29+
condition: getWeatherData(conds[Math.floor(Math.random() * conds.length)]!),
30+
humidity: Math.floor(Math.random() * 60) + 20,
31+
windSpeed: Math.floor(Math.random() * 25) + 5,
32+
dailyForecast: ['Today', 'Tomorrow', 'Thu', 'Fri', 'Sat'].map((day, i) => ({
33+
day,
34+
high: Math.round(temp + Math.random() * 8 - 2),
35+
low: Math.round(temp - Math.random() * 8 - 3),
36+
condition: getWeatherData(conds[(Math.floor(Math.random() * conds.length) + i) % conds.length]!)
37+
}))
38+
}
39+
}
40+
})

src/components/ModelSelect.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { model, models } = useModels()
66
const items = computed(() => models.map(model => ({
77
label: model,
88
value: model,
9-
icon: `i-simple-icons${model.split('/')[0]}`
9+
icon: `i-simple-icons:${model.split('/')[0]}`
1010
})))
1111
</script>
1212

src/components/Reasoning.vue

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script setup lang="ts">
2+
import { ref, watch } from 'vue'
3+
4+
const { isStreaming = false } = defineProps<{
5+
text: string
6+
isStreaming?: boolean
7+
}>()
8+
9+
const open = ref(false)
10+
11+
watch(() => isStreaming, () => {
12+
open.value = isStreaming
13+
}, { immediate: true })
14+
15+
function cleanMarkdown(text: string): string {
16+
return text
17+
.replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold
18+
.replace(/\*(.+?)\*/g, '$1') // Remove italic
19+
.replace(/`(.+?)`/g, '$1') // Remove inline code
20+
.replace(/^#+\s+/gm, '') // Remove headers
21+
}
22+
</script>
23+
24+
<template>
25+
<UCollapsible
26+
v-model:open="open"
27+
class="flex flex-col gap-1 my-5"
28+
>
29+
<UButton
30+
class="p-0 group"
31+
color="neutral"
32+
variant="link"
33+
trailing-icon="i-lucide-chevron-down"
34+
:ui="{
35+
trailingIcon: text.length > 0 ? 'group-data-[state=open]:rotate-180 transition-transform duration-200' : 'hidden'
36+
}"
37+
:label="isStreaming ? 'Thinking...' : 'Thoughts'"
38+
/>
39+
40+
<template #content>
41+
<div
42+
v-for="(value, index) in cleanMarkdown(text).split('\n').filter(Boolean)"
43+
:key="index"
44+
>
45+
<span class="whitespace-pre-wrap text-sm text-muted font-normal">{{ value }}</span>
46+
</div>
47+
</template>
48+
</UCollapsible>
49+
</template>

0 commit comments

Comments
 (0)