Skip to content

Commit 4d4392b

Browse files
committed
fix: mcp-toolkit cloudflare async + nuxthub 0.10
1 parent 6c5c67e commit 4d4392b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1568
-185
lines changed

.vitepress/auto-imports.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ declare global {
4848
const isRef: typeof import('vue').isRef
4949
const isShallow: typeof import('vue').isShallow
5050
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
51-
const manualResetRef: typeof import('@vueuse/core').manualResetRef
5251
const markRaw: typeof import('vue').markRaw
5352
const nextTick: typeof import('vue').nextTick
5453
const onActivated: typeof import('vue').onActivated
@@ -84,7 +83,6 @@ declare global {
8483
const refAutoReset: typeof import('@vueuse/core').refAutoReset
8584
const refDebounced: typeof import('@vueuse/core').refDebounced
8685
const refDefault: typeof import('@vueuse/core').refDefault
87-
const refManualReset: typeof import('@vueuse/core').refManualReset
8886
const refThrottled: typeof import('@vueuse/core').refThrottled
8987
const refWithControl: typeof import('@vueuse/core').refWithControl
9088
const resolveComponent: typeof import('vue').resolveComponent
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
defineProps<{ loading?: boolean }>()
5+
const emit = defineEmits<{ submit: [content: string] }>()
6+
7+
const input = ref('')
8+
9+
function handleSubmit() {
10+
const content = input.value.trim()
11+
if (!content)
12+
return
13+
emit('submit', content)
14+
input.value = ''
15+
}
16+
17+
function handleKeydown(e: KeyboardEvent) {
18+
if (e.key === 'Enter' && !e.shiftKey) {
19+
e.preventDefault()
20+
handleSubmit()
21+
}
22+
}
23+
</script>
24+
25+
<template>
26+
<form flex="~ gap-12" mt-16 @submit.prevent="handleSubmit">
27+
<textarea
28+
v-model="input" :disabled="loading"
29+
30+
border="~ neutral-300 focus:blue"
31+
text="14 neutral" placeholder="Ask about Nimiq..."
32+
ring="focus:1 focus:blue" outline-none rounded-6 bg-neutral-0 flex-1 max-h-200 min-h-48 resize-y transition-colors f-p-sm
33+
@keydown="handleKeydown"
34+
/>
35+
<button
36+
type="submit" :disabled="loading || !input.trim()"
37+
38+
flex="~ items-center justify-center gap-8"
39+
px-16 outline-none h-48 transition-opacity nq-pill nq-pill-blue disabled:opacity-50
40+
>
41+
<span v-if="loading" i-tabler:loader-2 size-18 animate-spin />
42+
<span v-else i-tabler:send size-18 />
43+
</button>
44+
</form>
45+
</template>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<script setup lang="ts">
2+
import type { UIMessage } from 'ai'
3+
import { computed } from 'vue'
4+
import { Conversation, ConversationContent, ConversationScrollButton, Message, MessageContent, MessageResponse, Progress, Reasoning, ReasoningContent, ReasoningTrigger, Source, Sources, SourcesContent, SourcesTrigger, Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '../ai-elements'
5+
import ChatInput from './ChatInput.vue'
6+
import { useChat } from './useChat'
7+
8+
const { messages, isLoading, progress, sendMessage, clearMessages } = useChat()
9+
10+
// Helper to get text content from message
11+
function getMessageText(msg: UIMessage): string {
12+
return msg.parts.filter(p => p.type === 'text').map(p => p.text).join('')
13+
}
14+
15+
// Helper to check if part is a tool part (type starts with 'tool-')
16+
function isToolPart(part: UIMessage['parts'][number]): part is UIMessage['parts'][number] & { toolCallId: string, state: string, input?: unknown, output?: unknown, errorText?: string } {
17+
return part.type.startsWith('tool-') && part.type !== 'tool'
18+
}
19+
20+
// Get tool name from tool part type
21+
function getToolName(part: UIMessage['parts'][number]): string {
22+
if (part.type.startsWith('tool-'))
23+
return part.type.slice(5) // Remove 'tool-' prefix
24+
return 'unknown'
25+
}
26+
27+
// Get first message text for header
28+
const headerTitle = computed(() => {
29+
const first = messages.value[0]
30+
if (!first)
31+
return ''
32+
const text = getMessageText(first)
33+
return text.length > 50 ? `${text.slice(0, 50)}...` : text
34+
})
35+
</script>
36+
37+
<template>
38+
<div class="chat-page nq-raw" flex="~ col" h="[calc(100vh-200px)]" min-h-400>
39+
<!-- Header (only when conversation started) -->
40+
<div v-if="messages.length" flex="~ items-center justify-between" mb-16>
41+
<div flex="~ items-center gap-12">
42+
<span text-blue size-24 i-nimiq:logos-nimiq />
43+
<h1 text="16 neutral" font-semibold m-0>
44+
{{ headerTitle }}
45+
</h1>
46+
</div>
47+
<button
48+
type="button"
49+
flex="~ items-center gap-6"
50+
text="12 neutral-700" px-12 py-6 outline-none rounded-full bg-neutral-200 cursor-pointer transition-colors
51+
@click="clearMessages"
52+
>
53+
<span i-tabler:trash size-14 />
54+
<span>Clear</span>
55+
</button>
56+
</div>
57+
58+
<!-- Conversation -->
59+
<Conversation flex-1 overflow-hidden>
60+
<ConversationContent>
61+
<!-- Empty state -->
62+
<div v-if="!messages.length" flex="~ col items-center justify-center" text-center h-full>
63+
<span text-blue mb-16 size-48 i-nimiq:logos-nimiq />
64+
<h2 text="18 neutral" font-semibold mb-8>
65+
Nimiq Developer Assistant
66+
</h2>
67+
<p text="14 neutral/60" max-w-400>
68+
Ask questions about Nimiq blockchain development, APIs, and documentation.
69+
</p>
70+
</div>
71+
72+
<!-- Messages -->
73+
<template v-for="msg in messages" :key="msg.id">
74+
<Message :from="msg.role">
75+
<div class="flex flex-col gap-2 w-full">
76+
<!-- Group sources at top for assistant messages -->
77+
<Sources v-if="msg.role === 'assistant' && msg.parts.filter(p => p.type === 'source-url').length">
78+
<SourcesTrigger :count="msg.parts.filter(p => p.type === 'source-url').length" />
79+
<SourcesContent>
80+
<template v-for="(part, i) in msg.parts" :key="`${msg.id}-source-${i}`">
81+
<Source v-if="part.type === 'source-url'" :href="part.url" :title="part.title ?? 'Source'" />
82+
</template>
83+
</SourcesContent>
84+
</Sources>
85+
86+
<!-- Render parts -->
87+
<template v-for="(part, i) in msg.parts" :key="`${msg.id}-${i}`">
88+
<!-- Reasoning -->
89+
<Reasoning v-if="part.type === 'reasoning'" :is-streaming="part.state === 'streaming'">
90+
<ReasoningTrigger />
91+
<ReasoningContent :content="part.text" />
92+
</Reasoning>
93+
94+
<!-- Tool calls (type is 'tool-{toolName}') -->
95+
<Tool v-else-if="isToolPart(part)" :default-open="part.state !== 'output-available'">
96+
<ToolHeader :state="part.state" :title="getToolName(part)" />
97+
<ToolContent>
98+
<ToolInput :input="part.input" />
99+
<ToolOutput v-if="part.output || part.errorText" :output="part.output" :error-text="part.errorText" />
100+
</ToolContent>
101+
</Tool>
102+
103+
<!-- Text content -->
104+
<MessageContent v-else-if="part.type === 'text'">
105+
<MessageResponse :content="part.text" />
106+
</MessageContent>
107+
108+
<!-- Skip source-url here since we handle them above -->
109+
</template>
110+
</div>
111+
</Message>
112+
</template>
113+
114+
<!-- Progress indicator while searching/generating -->
115+
<Progress v-if="isLoading && progress.step" :step="progress.step" :message="progress.message" />
116+
</ConversationContent>
117+
<ConversationScrollButton />
118+
</Conversation>
119+
120+
<!-- Input -->
121+
<ChatInput :loading="isLoading" @submit="sendMessage" />
122+
</div>
123+
</template>
124+
125+
<style>
126+
/* Remove article padding when chat page is present */
127+
article.nq-prose:has(.chat-page) {
128+
padding-bottom: 0 !important;
129+
}
130+
article.nq-prose:has(.chat-page) > div {
131+
padding-bottom: 0 !important;
132+
height: 100%;
133+
}
134+
</style>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { UIMessage } from 'ai'
2+
import { Chat } from '@ai-sdk/vue'
3+
import { computed, ref } from 'vue'
4+
5+
declare const __NIMIQ_API_URL__: string
6+
const API_URL = typeof __NIMIQ_API_URL__ !== 'undefined' ? __NIMIQ_API_URL__ : 'http://localhost:3000'
7+
8+
export interface ProgressState {
9+
step: string
10+
message: string
11+
}
12+
13+
// Custom data types for our chat
14+
interface ChatDataTypes {
15+
progress: ProgressState
16+
}
17+
18+
export function useChat() {
19+
const progress = ref<ProgressState>({ step: '', message: '' })
20+
21+
const chat = new Chat<UIMessage<unknown, ChatDataTypes>>({
22+
api: `${API_URL}/api/chat`,
23+
onData(data) {
24+
// Handle custom data parts like progress (type is 'data-progress', data in .data)
25+
if (data && typeof data === 'object' && 'type' in data) {
26+
if (data.type === 'data-progress' && 'data' in data) {
27+
const progressData = (data as any).data
28+
progress.value = { step: progressData.step, message: progressData.message }
29+
}
30+
}
31+
},
32+
})
33+
34+
const messages = computed(() => chat.messages.value)
35+
const isLoading = computed(() => chat.status.value === 'streaming' || chat.status.value === 'submitted')
36+
37+
async function sendMessage(content: string) {
38+
// Reset progress when starting new message
39+
progress.value = { step: '', message: '' }
40+
await chat.sendMessage({ text: content })
41+
}
42+
43+
function clearMessages() {
44+
chat.setMessages([])
45+
progress.value = { step: '', message: '' }
46+
}
47+
48+
return {
49+
messages,
50+
isLoading,
51+
progress,
52+
sendMessage,
53+
clearMessages,
54+
}
55+
}
56+
57+
// Re-export types from AI SDK
58+
export type { UIMessage }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts">
2+
import { StickToBottom } from 'vue-stick-to-bottom'
3+
4+
withDefaults(defineProps<{
5+
ariaLabel?: string
6+
initial?: boolean | 'instant' | { damping?: number, stiffness?: number, mass?: number }
7+
damping?: number
8+
stiffness?: number
9+
mass?: number
10+
}>(), { ariaLabel: 'Conversation', initial: true, damping: 0.7, stiffness: 0.05, mass: 1.25 })
11+
</script>
12+
13+
<template>
14+
<StickToBottom class="flex-1 relative overflow-y-hidden" role="log" :aria-label="ariaLabel" :initial="initial" :damping="damping" :stiffness="stiffness" :mass="mass" anchor="none">
15+
<slot />
16+
</StickToBottom>
17+
</template>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script setup lang="ts">
2+
</script>
3+
4+
<template>
5+
<div class="p-4 flex flex-col gap-6">
6+
<slot />
7+
</div>
8+
</template>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { useStickToBottomContext } from 'vue-stick-to-bottom'
4+
5+
const { isAtBottom, scrollToBottom } = useStickToBottomContext()
6+
const showScrollButton = computed(() => !isAtBottom.value)
7+
8+
function handleClick() {
9+
scrollToBottom()
10+
}
11+
</script>
12+
13+
<template>
14+
<button v-if="showScrollButton" type="button" class="p-2 border border-neutral/10 rounded-full bg-white cursor-pointer shadow-lg transition-colors bottom-4 left-1/2 absolute dark:bg-darkblue hover:bg-neutral/5 -translate-x-1/2" @click="handleClick">
15+
<div class="i-tabler:arrow-down size-4" />
16+
</button>
17+
</template>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as Conversation } from './Conversation.vue'
2+
export { default as ConversationContent } from './ConversationContent.vue'
3+
export { default as ConversationScrollButton } from './ConversationScrollButton.vue'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export * from './conversation'
2+
export * from './message'
3+
export * from './progress'
4+
export * from './reasoning'
5+
export * from './shimmer'
6+
export * from './sources'
7+
export * from './tool'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script setup lang="ts">
2+
defineProps<{ from: 'user' | 'assistant' | 'system' }>()
3+
</script>
4+
5+
<template>
6+
<div class="group flex gap-2 max-w-[85%] w-full" :class="from === 'user' ? 'is-user ml-auto justify-end' : 'is-assistant'">
7+
<slot />
8+
</div>
9+
</template>

0 commit comments

Comments
 (0)