@mirodan/ai-insight 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @mirodan/ai-insight
2
+
3
+ AI-powered insight tooltip component for Nuxt UI.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @mirodan/ai-insight
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```vue
14
+ <template>
15
+ <AiInsight
16
+ :data="chartData"
17
+ title="Sales Report"
18
+ :prompt="'Analyze sales trends'"
19
+ >
20
+ <LineChart :data="chartData" />
21
+ </AiInsight>
22
+ </template>
23
+
24
+ <script setup>
25
+ import { AiInsight } from '@mirodan/ai-insight'
26
+ </script>
27
+ ```
28
+
29
+ ## License
30
+
31
+ MIT
@@ -0,0 +1,6 @@
1
+ declare const _default: import("@nuxt/schema").NuxtModule<{
2
+ endpoint: string;
3
+ }, {
4
+ endpoint: string;
5
+ }, false>;
6
+ export default _default;
package/dist/module.js ADDED
@@ -0,0 +1,26 @@
1
+ import { addComponent as e, addServerHandler as t, createResolver as n, defineNuxtModule as r } from "@nuxt/kit";
2
+ //#region src/module.ts
3
+ var i = r({
4
+ meta: { name: "@mirodan/ai-insight" },
5
+ defaults: { endpoint: "https://api.mirodan.ai/v1/insight" },
6
+ setup(r, i) {
7
+ let a = n(import.meta.url);
8
+ i.options.runtimeConfig.aiInsight = {
9
+ key: "",
10
+ endpoint: r.endpoint
11
+ }, e({
12
+ name: "AiInsight",
13
+ filePath: a.resolve("./runtime/components/AiInsight.vue")
14
+ }), e({
15
+ name: "AiInsightDialog",
16
+ filePath: a.resolve("./runtime/components/AiInsightDialog.vue")
17
+ }), t({
18
+ route: "/api/ai-insight/proxy",
19
+ handler: a.resolve("./runtime/server/proxy.post.ts")
20
+ });
21
+ }
22
+ });
23
+ //#endregion
24
+ export { i as default };
25
+
26
+ //# sourceMappingURL=module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.js","names":[],"sources":["../src/module.ts"],"sourcesContent":["import { defineNuxtModule, createResolver, addComponent, addServerHandler } from '@nuxt/kit'\n\nexport default defineNuxtModule({\n meta: { name: '@mirodan/ai-insight' },\n defaults: { endpoint: 'https://api.mirodan.ai/v1/insight' },\n setup(options, nuxt) {\n const resolver = createResolver(import.meta.url)\n\n nuxt.options.runtimeConfig.aiInsight = {\n key: '',\n endpoint: options.endpoint\n }\n\n addComponent({\n name: 'AiInsight',\n filePath: resolver.resolve('./runtime/components/AiInsight.vue')\n })\n\n addComponent({\n name: 'AiInsightDialog',\n filePath: resolver.resolve('./runtime/components/AiInsightDialog.vue')\n })\n\n addServerHandler({\n route: '/api/ai-insight/proxy',\n handler: resolver.resolve('./runtime/server/proxy.post.ts')\n })\n }\n})\n"],"mappings":";;AAEA,IAAA,IAAe,EAAiB;CAC9B,MAAM,EAAE,MAAM,sBAAsB;CACpC,UAAU,EAAE,UAAU,oCAAoC;CAC1D,MAAM,GAAS,GAAM;EACnB,IAAM,IAAW,EAAe,YAAY,GAAG;EAiB/C,AAfA,EAAK,QAAQ,cAAc,YAAY;GACrC,KAAK;GACL,UAAU,EAAQ;EACpB,GAEA,EAAa;GACX,MAAM;GACN,UAAU,EAAS,QAAQ,oCAAoC;EACjE,CAAC,GAED,EAAa;GACX,MAAM;GACN,UAAU,EAAS,QAAQ,0CAA0C;EACvE,CAAC,GAED,EAAiB;GACf,OAAO;GACP,SAAS,EAAS,QAAQ,gCAAgC;EAC5D,CAAC;CACH;AACF,CAAC"}
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+
4
+ interface Props {
5
+ data: unknown
6
+ title?: string
7
+ prompt?: string
8
+ position?: 'top' | 'bottom'
9
+ }
10
+
11
+ withDefaults(defineProps<Props>(), { position: 'top' })
12
+ const dialogOpen = ref(false)
13
+ </script>
14
+ <template>
15
+ <div class="relative">
16
+ <slot />
17
+ <div
18
+ v-if="!dialogOpen"
19
+ class="absolute z-10 flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground cursor-pointer transition-opacity hover:opacity-80"
20
+ :class="position === 'top' ? 'bottom-full left-1/2 -translate-x-1/2 mb-2' : 'top-full left-1/2 -translate-x-1/2 mt-2'"
21
+ @click="dialogOpen = true"
22
+ >
23
+ <span>AI анализ</span>
24
+ <UIcon
25
+ name="i-lucide-brain"
26
+ class="size-3.5"
27
+ />
28
+ </div>
29
+ <AiInsightDialog
30
+ v-model:open="dialogOpen"
31
+ :data
32
+ :title
33
+ :prompt
34
+ />
35
+ </div>
36
+ </template>
@@ -0,0 +1,256 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, onMounted, nextTick } from 'vue'
3
+ import { useDraggable } from '@vueuse/core'
4
+ import { parseUIMessageStream } from '../../utils/parseUIMessageStream'
5
+
6
+ interface Props {
7
+ open: boolean
8
+ data: unknown
9
+ title?: string
10
+ prompt?: string
11
+ locale?: string
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), { open: false })
15
+ const emit = defineEmits<{ 'update:open': [value: boolean] }>()
16
+
17
+ const panelEl = ref<HTMLElement | null>(null)
18
+ const headerEl = ref<HTMLElement | null>(null)
19
+ const panelWidth = ref(440)
20
+ const panelHeight = ref(400)
21
+ const resizing = ref(false)
22
+ const { x, y, isDragging } = useDraggable(panelEl, {
23
+ handle: headerEl,
24
+ initialValue: { x: 800, y: 80 }
25
+ })
26
+
27
+ onMounted(() => {
28
+ x.value = window.innerWidth - panelWidth.value - 20
29
+ })
30
+
31
+ function onResizeStart(e: PointerEvent) {
32
+ resizing.value = true
33
+ const startX = e.clientX
34
+ const startY = e.clientY
35
+ const startW = panelWidth.value
36
+ const startH = panelEl.value?.offsetHeight ?? 400
37
+
38
+ function onMove(ev: PointerEvent) {
39
+ panelWidth.value = Math.max(280, startW + ev.clientX - startX)
40
+ panelHeight.value = Math.max(200, startH + ev.clientY - startY)
41
+ }
42
+ function onUp() {
43
+ resizing.value = false
44
+ window.removeEventListener('pointermove', onMove)
45
+ window.removeEventListener('pointerup', onUp)
46
+ }
47
+ window.addEventListener('pointermove', onMove)
48
+ window.addEventListener('pointerup', onUp)
49
+ }
50
+
51
+ const insight = ref('')
52
+ const loading = ref(false)
53
+ const errorMsg = ref('')
54
+ const followUpInput = ref('')
55
+ const followUpMessages = ref<Array<{ role: 'user' | 'assistant', content: string }>>([])
56
+ const currentPrompt = ref('')
57
+
58
+ async function loadInsight() {
59
+ loading.value = true
60
+ errorMsg.value = ''
61
+ insight.value = ''
62
+ followUpMessages.value = []
63
+ currentPrompt.value = ''
64
+
65
+ const locale = props.locale || navigator.language
66
+
67
+ try {
68
+ const response = await fetch('/api/ai-insight/proxy', {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({
72
+ data: props.data,
73
+ prompt: currentPrompt.value || props.prompt,
74
+ locale
75
+ })
76
+ })
77
+
78
+ if (!response.ok) {
79
+ const text = await response.text()
80
+ throw new Error(text || `HTTP ${response.status}`)
81
+ }
82
+
83
+ let result = ''
84
+ for await (const delta of parseUIMessageStream(response)) {
85
+ result += delta
86
+ insight.value = result
87
+ }
88
+ } catch (e) {
89
+ errorMsg.value = e instanceof Error ? e.message : 'Unknown error'
90
+ } finally {
91
+ loading.value = false
92
+ }
93
+ }
94
+
95
+ async function sendFollowUp() {
96
+ if (!followUpInput.value.trim()) return
97
+ const text = followUpInput.value
98
+ followUpInput.value = ''
99
+ followUpMessages.value.push({ role: 'user', content: text })
100
+
101
+ currentPrompt.value = `Context: ${insight.value}\n\nQuestion: ${text}`
102
+ await loadInsight()
103
+ }
104
+
105
+ function close() {
106
+ emit('update:open', false)
107
+ }
108
+
109
+ function refresh() {
110
+ loadInsight()
111
+ }
112
+
113
+ onMounted(() => {
114
+ if (props.open) loadInsight()
115
+ })
116
+
117
+ watch(() => props.open, (val) => {
118
+ if (val) nextTick(() => loadInsight())
119
+ })
120
+ </script>
121
+ <template>
122
+ <Teleport to="body">
123
+ <div
124
+ v-if="open"
125
+ ref="panelEl"
126
+ class="ai-insight-panel fixed z-50 flex flex-col rounded-lg border bg-background text-foreground shadow-xl"
127
+ :class="{ 'cursor-grabbing select-none': isDragging, 'cursor-se-resize select-none': resizing }"
128
+ :style="{ left: `${x}px`, top: `${y}px`, width: `${panelWidth}px`, height: `${panelHeight}px` }"
129
+ >
130
+ <div
131
+ ref="headerEl"
132
+ class="flex cursor-grab items-center justify-between rounded-t-lg border-b px-4 py-3"
133
+ >
134
+ <h3 class="text-sm font-medium">
135
+ {{ title || 'AI Insight' }}
136
+ </h3>
137
+ <div class="flex items-center gap-1">
138
+ <UButton
139
+ icon="i-lucide-refresh-cw"
140
+ size="xs"
141
+ variant="ghost"
142
+ color="neutral"
143
+ @click="refresh"
144
+ />
145
+ <UButton
146
+ icon="i-lucide-x"
147
+ size="xs"
148
+ variant="ghost"
149
+ color="neutral"
150
+ @click="close"
151
+ />
152
+ </div>
153
+ </div>
154
+
155
+ <div class="flex-1 overflow-y-auto px-4 py-3">
156
+ <div class="space-y-4">
157
+ <div
158
+ v-if="loading && !insight"
159
+ class="flex items-center gap-2 text-sm text-muted-foreground"
160
+ >
161
+ <UIcon
162
+ name="i-lucide-loader-2"
163
+ class="size-4 animate-spin"
164
+ />
165
+ <span>Analyzing...</span>
166
+ </div>
167
+
168
+ <div
169
+ v-else-if="errorMsg"
170
+ class="rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error"
171
+ >
172
+ <div class="flex items-start gap-2">
173
+ <UIcon
174
+ name="i-lucide-alert-circle"
175
+ class="size-4 mt-0.5 shrink-0"
176
+ />
177
+ <div>
178
+ <p class="font-medium">
179
+ {{ errorMsg }}
180
+ </p>
181
+ <UButton
182
+ variant="ghost"
183
+ color="neutral"
184
+ size="xs"
185
+ class="mt-2"
186
+ @click="refresh"
187
+ >
188
+ Retry
189
+ </UButton>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <div
195
+ v-else-if="insight"
196
+ class="prose prose-sm max-w-none"
197
+ >
198
+ <ChatComark :markdown="insight" />
199
+ </div>
200
+
201
+ <div
202
+ v-else
203
+ class="text-sm text-muted-foreground"
204
+ >
205
+ {{ loading ? 'Analyzing...' : 'No data available' }}
206
+ </div>
207
+
208
+ <div
209
+ v-if="insight && !loading"
210
+ class="border-t pt-4"
211
+ >
212
+ <div class="flex flex-col items-center space-y-3">
213
+ <div
214
+ v-for="(msg, i) in followUpMessages"
215
+ :key="i"
216
+ :class="msg.role === 'user' ? 'text-right self-end' : 'text-left self-start'"
217
+ >
218
+ <span
219
+ class="inline-block rounded-lg px-3 py-2 text-sm"
220
+ :class="msg.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted'"
221
+ >
222
+ {{ msg.content }}
223
+ </span>
224
+ </div>
225
+ <UInput
226
+ v-model="followUpInput"
227
+ placeholder="Ask a follow-up..."
228
+ class="max-w-md w-full"
229
+ size="lg"
230
+ @keydown.enter="sendFollowUp"
231
+ >
232
+ <template #trailing>
233
+ <UButton
234
+ icon="i-lucide-send"
235
+ size="xs"
236
+ :disabled="!followUpInput.trim() || loading"
237
+ @click="sendFollowUp"
238
+ />
239
+ </template>
240
+ </UInput>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ </div>
245
+
246
+ <div
247
+ class="absolute bottom-0 right-0 z-10 h-4 w-4 cursor-se-resize"
248
+ @pointerdown.stop="onResizeStart"
249
+ />
250
+ </div>
251
+ </Teleport>
252
+ </template>
253
+ <style scoped>
254
+ .ai-insight-panel { min-height: 200px; }
255
+ .ai-insight-panel :deep(.prose) { word-break: break-word; }
256
+ </style>
@@ -0,0 +1,22 @@
1
+ export default defineEventHandler(async (event) => {
2
+ const config = useRuntimeConfig(event)
3
+ const body = await readBody(event)
4
+
5
+ const response = await $fetch.raw(config.aiInsight.endpoint, {
6
+ method: 'POST',
7
+ headers: {
8
+ Authorization: `Bearer ${config.aiInsight.key}`,
9
+ 'Content-Type': 'application/json'
10
+ },
11
+ body,
12
+ responseType: 'stream'
13
+ })
14
+
15
+ setHeaders(event, {
16
+ 'Content-Type': 'text/event-stream',
17
+ 'Cache-Control': 'no-cache',
18
+ Connection: 'keep-alive'
19
+ })
20
+
21
+ return response.body
22
+ })
@@ -0,0 +1,13 @@
1
+ export interface AiInsightProps {
2
+ data: unknown;
3
+ title?: string;
4
+ prompt?: string;
5
+ position?: 'top' | 'bottom';
6
+ }
7
+ export interface AiInsightDialogProps {
8
+ open: boolean;
9
+ data: unknown;
10
+ title?: string;
11
+ prompt?: string;
12
+ locale?: string;
13
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@mirodan/ai-insight",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered insight tooltip component for Nuxt",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "./dist/module.mjs",
8
+ "types": "./dist/module.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/module.d.ts",
12
+ "import": "./dist/module.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "vite build && node scripts/copy-runtime.mjs && tsc -p tsconfig.build.json",
20
+ "dev": "vite build --watch",
21
+ "prepare": "vite build && node scripts/copy-runtime.mjs && tsc -p tsconfig.build.json",
22
+ "prepublishOnly": "npm run build",
23
+ "lint": "eslint .",
24
+ "test": "vitest run"
25
+ },
26
+ "dependencies": {
27
+ "@vueuse/core": "^14.0.0",
28
+ "defu": "^6.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "@nuxt/eslint": "^1.16.0",
32
+ "@types/node": "^26.1.2",
33
+ "@vitejs/plugin-vue": "^6.0.8",
34
+ "eslint": "^10.8.0",
35
+ "happy-dom": "^17.4.4",
36
+ "typescript": "^5.9.3",
37
+ "typescript-eslint": "^8.65.0",
38
+ "vite": "^8.2.0",
39
+ "vitest": "^3.1.2"
40
+ },
41
+ "peerDependencies": {
42
+ "@comark/nuxt": "^0.4.0",
43
+ "@iconify-json/lucide": "^1.0.0",
44
+ "@nuxt/kit": "^4.0.0",
45
+ "@nuxt/ui": "^4.0.0",
46
+ "nuxt": "^4.0.0",
47
+ "tailwindcss": "^4.0.0",
48
+ "vue": "^3.0.0"
49
+ }
50
+ }