@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 +31 -0
- package/dist/module.d.ts +6 -0
- package/dist/module.js +26 -0
- package/dist/module.js.map +1 -0
- package/dist/runtime/components/AiInsight.vue +36 -0
- package/dist/runtime/components/AiInsightDialog.vue +256 -0
- package/dist/runtime/server/proxy.post.ts +22 -0
- package/dist/types/index.d.ts +13 -0
- package/package.json +50 -0
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
|
package/dist/module.d.ts
ADDED
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
|
+
}
|