Skip to content

Commit de781b5

Browse files
feat(reasoning): add component (#75)
Co-authored-by: Benjamin Canac <[email protected]>
1 parent c5a725f commit de781b5

File tree

6 files changed

+70
-25
lines changed

6 files changed

+70
-25
lines changed

app/components/Reasoning.vue

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

app/components/tool/Chart.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const props = defineProps<{
55
66
const color = computed(() => {
77
return ({
8-
'output-available': 'bg-gradient-to-br from-sky-400 via-blue-500 to-indigo-600 dark:from-sky-500 dark:via-blue-600 dark:to-indigo-700',
98
'output-error': 'bg-muted text-error'
109
})[props.invocation.state as string] || 'bg-muted text-white'
1110
})

app/components/tool/Weather.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const props = defineProps<{
55
66
const color = computed(() => {
77
return ({
8-
'output-available': 'bg-gradient-to-br from-sky-400 via-blue-500 to-indigo-600 dark:from-sky-500 dark:via-blue-600 dark:to-indigo-700',
8+
'output-available': 'bg-gradient-to-br from-sky-400 via-blue-500 to-indigo-600 dark:from-sky-500 dark:via-blue-600 dark:to-indigo-700 text-white',
99
'output-error': 'bg-muted text-error'
1010
})[props.invocation.state as string] || 'bg-muted text-white'
1111
})
@@ -29,9 +29,9 @@ const message = computed(() => {
2929
<div class="rounded-xl px-5 py-4" :class="color">
3030
<template v-if="invocation.state === 'output-available'">
3131
<div class="flex items-start justify-between mb-3">
32-
<div class="flex items-baseline gap-1">
33-
<span class="text-4xl font-light">{{ invocation.output.temperature }}°</span>
34-
<span class="text-base text-white/80 mt-1">C</span>
32+
<div class="flex items-baseline">
33+
<span class="text-4xl font-bold">{{ invocation.output.temperature }}°</span>
34+
<span class="text-base text-white/80">C</span>
3535
</div>
3636
<div class="text-right">
3737
<div class="text-base font-medium mb-1">

app/composables/useModels.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
export function useModels() {
22
const models = [
3-
// OpenAI Models
4-
'openai/gpt-5',
5-
'openai/gpt-5-mini',
6-
'openai/gpt-4o',
7-
'openai/gpt-4o-mini',
8-
// Anthropic Claude Models
9-
'anthropic/claude-sonnet-4',
10-
'anthropic/claude-sonnet-3.7',
11-
// Google Gemini Models
12-
'google/gemini-2.5-pro',
3+
'openai/gpt-5-nano',
4+
'anthropic/claude-haiku-4.5',
135
'google/gemini-2.5-flash'
146
]
157

16-
const model = useCookie<string>('model', { default: () => 'openai/gpt-4o-mini' })
8+
const model = useCookie<string>('model', { default: () => 'openai/gpt-5-nano' })
179

1810
return {
1911
models,

app/pages/chat/[id].vue

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,24 +93,22 @@ onMounted(() => {
9393
:messages="chat.messages"
9494
:status="chat.status"
9595
:assistant="chat.status !== 'streaming' ? { actions: [{ label: 'Copy', icon: copied ? 'i-lucide-copy-check' : 'i-lucide-copy', onClick: copy }] } : { actions: [] }"
96-
class="lg:pt-(--ui-header-height) pb-4 sm:pb-6"
9796
:spacing-offset="160"
97+
class="lg:pt-(--ui-header-height) pb-4 sm:pb-6"
9898
>
9999
<template #content="{ message }">
100-
<div class="space-y-5">
100+
<div class="*:first:!mt-0 *:last:!mb-0 space-y-5">
101101
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`">
102-
<UButton
102+
<Reasoning
103103
v-if="part.type === 'reasoning'"
104-
:label="part.state === 'done' ? 'Done' : 'Thinking...'"
105-
variant="link"
106-
color="neutral"
107-
class="px-0"
104+
:text="part.text"
105+
:is-streaming="part.state !== 'done'"
108106
/>
109107
<MDCCached
110108
v-else-if="part.type === 'text'"
111109
:value="part.text"
112110
:cache-key="`${message.id}-${index}`"
113-
unwrap="p"
111+
:unwrap="message.role === 'user' ? 'p' : undefined"
114112
:components="components"
115113
:parser-options="{ highlight: false }"
116114
/>

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,22 @@ export default defineEventHandler(async (event) => {
6262
execute: ({ writer }) => {
6363
const result = streamText({
6464
model: gateway(model),
65-
system: `You are a helpful assistant that can answer questions and help. ${session.user?.username ? `User name is ${session.user?.username}.` : ''} (if not provided, use fake data for tool calls)`,
65+
system: `You are a knowledgeable and helpful AI assistant. ${session.user?.username ? `The user's name is ${session.user.username}.` : ''} Your goal is to provide clear, accurate, and well-structured responses.
66+
67+
**FORMATTING RULES (CRITICAL):**
68+
- ABSOLUTELY NO MARKDOWN HEADINGS: Never use #, ##, ###, ####, #####, or ######
69+
- NO underline-style headings with === or ---
70+
- Use **bold text** for emphasis and section labels instead
71+
- Examples:
72+
* Instead of "## Usage", write "**Usage:**" or just "Here's how to use it:"
73+
* Instead of "# Complete Guide", write "**Complete Guide**" or start directly with content
74+
- Start all responses with content, never with a heading
75+
76+
**RESPONSE QUALITY:**
77+
- Be concise yet comprehensive
78+
- Use examples when helpful
79+
- Break down complex topics into digestible parts
80+
- Maintain a friendly, professional tone`,
6681
messages: convertToModelMessages(messages),
6782
providerOptions: {
6883
openai: {

0 commit comments

Comments
 (0)