@mirodan/ai-insight 0.1.3 → 0.1.5

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/dist/module.js CHANGED
@@ -1,26 +1,26 @@
1
- import { addComponent as e, addServerHandler as t, createResolver as n, defineNuxtModule as r } from "@nuxt/kit";
1
+ import { addComponent as e, addPlugin as t, addServerHandler as n, createResolver as r, defineNuxtModule as i } from "@nuxt/kit";
2
2
  //#region src/module.ts
3
- var i = r({
3
+ var a = i({
4
4
  meta: { name: "@mirodan/ai-insight" },
5
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 = {
6
+ setup(i, a) {
7
+ let o = r(import.meta.url);
8
+ a.options.runtimeConfig.aiInsight = {
9
9
  key: "",
10
- endpoint: r.endpoint
10
+ endpoint: i.endpoint
11
11
  }, e({
12
12
  name: "AiInsight",
13
- filePath: a.resolve("./runtime/components/AiInsight.vue")
13
+ filePath: o.resolve("./runtime/components/AiInsight.vue")
14
14
  }), e({
15
15
  name: "AiInsightDialog",
16
- filePath: a.resolve("./runtime/components/AiInsightDialog.vue")
17
- }), t({
16
+ filePath: o.resolve("./runtime/components/AiInsightDialog.vue")
17
+ }), n({
18
18
  route: "/api/ai-insight/proxy",
19
- handler: a.resolve("./runtime/server/proxy.post.ts")
20
- });
19
+ handler: o.resolve("./runtime/server/proxy.post.ts")
20
+ }), t(o.resolve("./runtime/plugins/i18n.ts"));
21
21
  }
22
22
  });
23
23
  //#endregion
24
- export { i as default };
24
+ export { a as default };
25
25
 
26
26
  //# sourceMappingURL=module.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"module.js","names":[],"sources":["../src/module.ts"],"sourcesContent":["import { defineNuxtModule, createResolver, addComponent, addServerHandler, addPlugin } 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 addPlugin(resolver.resolve('./runtime/plugins/i18n.ts'))\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;EAsB/C,AApBA,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,GAED,EAAU,EAAS,QAAQ,2BAA2B,CAAC;CACzD;AACF,CAAC"}
@@ -16,7 +16,7 @@ const dialogOpen = ref(false)
16
16
  <slot />
17
17
  <div
18
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"
19
+ class="absolute z-10 flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs text-muted cursor-pointer transition-opacity hover:opacity-80"
20
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
21
  @click="dialogOpen = true"
22
22
  >
@@ -1,7 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch, onMounted, nextTick } from 'vue'
3
3
  import { useDraggable } from '@vueuse/core'
4
- import { parseUIMessageStream } from '../../utils/parseUIMessageStream'
4
+ import { useI18n } from 'vue-i18n'
5
+ import { parseUIMessageStream } from '../utils/parseUIMessageStream'
6
+
7
+ const { t, locale: i18nLocale } = useI18n()
5
8
 
6
9
  interface Props {
7
10
  open: boolean
@@ -62,7 +65,7 @@ async function loadInsight() {
62
65
  followUpMessages.value = []
63
66
  currentPrompt.value = ''
64
67
 
65
- const locale = props.locale || navigator.language
68
+ const locale = props.locale || i18nLocale?.value || navigator.language
66
69
 
67
70
  try {
68
71
  const response = await fetch('/api/ai-insight/proxy', {
@@ -76,8 +79,15 @@ async function loadInsight() {
76
79
  })
77
80
 
78
81
  if (!response.ok) {
79
- const text = await response.text()
80
- throw new Error(text || `HTTP ${response.status}`)
82
+ let msg = t('aiInsight.requestError', { status: response.status })
83
+ try {
84
+ const json = await response.json()
85
+ msg = json.statusMessage || json.message || msg
86
+ } catch {
87
+ const text = await response.text().catch(() => '')
88
+ if (text) msg = text
89
+ }
90
+ throw new Error(msg)
81
91
  }
82
92
 
83
93
  let result = ''
@@ -86,7 +96,7 @@ async function loadInsight() {
86
96
  insight.value = result
87
97
  }
88
98
  } catch (e) {
89
- errorMsg.value = e instanceof Error ? e.message : 'Unknown error'
99
+ errorMsg.value = e instanceof Error ? e.message : t('aiInsight.unknownError')
90
100
  } finally {
91
101
  loading.value = false
92
102
  }
@@ -123,16 +133,16 @@ watch(() => props.open, (val) => {
123
133
  <div
124
134
  v-if="open"
125
135
  ref="panelEl"
126
- class="ai-insight-panel fixed z-50 flex flex-col rounded-lg border bg-background text-foreground shadow-xl"
136
+ class="ai-insight-panel fixed z-50 flex flex-col rounded-lg border border-default bg-default text-default text-sm shadow-xl"
127
137
  :class="{ 'cursor-grabbing select-none': isDragging, 'cursor-se-resize select-none': resizing }"
128
138
  :style="{ left: `${x}px`, top: `${y}px`, width: `${panelWidth}px`, height: `${panelHeight}px` }"
129
139
  >
130
140
  <div
131
141
  ref="headerEl"
132
- class="flex cursor-grab items-center justify-between rounded-t-lg border-b px-4 py-3"
142
+ class="flex cursor-grab items-center justify-between rounded-t-lg border-b border-default px-4 py-3"
133
143
  >
134
144
  <h3 class="text-sm font-medium">
135
- {{ title || 'AI Insight' }}
145
+ {{ title || t('aiInsight.title') }}
136
146
  </h3>
137
147
  <div class="flex items-center gap-1">
138
148
  <UButton
@@ -156,13 +166,13 @@ watch(() => props.open, (val) => {
156
166
  <div class="space-y-4">
157
167
  <div
158
168
  v-if="loading && !insight"
159
- class="flex items-center gap-2 text-sm text-muted-foreground"
169
+ class="flex items-center gap-2 text-sm text-muted"
160
170
  >
161
171
  <UIcon
162
172
  name="i-lucide-loader-2"
163
173
  class="size-4 animate-spin"
164
174
  />
165
- <span>Analyzing...</span>
175
+ <span>{{ t('aiInsight.analyzing') }}</span>
166
176
  </div>
167
177
 
168
178
  <div
@@ -185,7 +195,7 @@ watch(() => props.open, (val) => {
185
195
  class="mt-2"
186
196
  @click="refresh"
187
197
  >
188
- Retry
198
+ {{ t('aiInsight.retry') }}
189
199
  </UButton>
190
200
  </div>
191
201
  </div>
@@ -200,46 +210,46 @@ watch(() => props.open, (val) => {
200
210
 
201
211
  <div
202
212
  v-else
203
- class="text-sm text-muted-foreground"
213
+ class="text-sm text-muted"
204
214
  >
205
- {{ loading ? 'Analyzing...' : 'No data available' }}
215
+ {{ loading ? t('aiInsight.analyzing') : t('aiInsight.noData') }}
206
216
  </div>
217
+ </div>
218
+ </div>
207
219
 
220
+ <div
221
+ v-if="insight && !loading"
222
+ class="shrink-0 border-t border-default px-4 py-3"
223
+ >
224
+ <div class="flex flex-col items-center space-y-2">
208
225
  <div
209
- v-if="insight && !loading"
210
- class="border-t pt-4"
226
+ v-for="(msg, i) in followUpMessages"
227
+ :key="i"
228
+ :class="msg.role === 'user' ? 'text-right self-end' : 'text-left self-start'"
211
229
  >
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>
230
+ <span
231
+ class="inline-block rounded-lg px-3 py-1.5 text-sm"
232
+ :class="msg.role === 'user' ? 'bg-primary text-white' : 'bg-muted'"
233
+ >
234
+ {{ msg.content }}
235
+ </span>
242
236
  </div>
237
+ <UInput
238
+ v-model="followUpInput"
239
+ :placeholder="t('aiInsight.askQuestion')"
240
+ class="max-w-md w-full"
241
+ size="sm"
242
+ @keydown.enter="sendFollowUp"
243
+ >
244
+ <template #trailing>
245
+ <UButton
246
+ icon="i-lucide-send"
247
+ size="xs"
248
+ :disabled="!followUpInput.trim() || loading"
249
+ @click="sendFollowUp"
250
+ />
251
+ </template>
252
+ </UInput>
243
253
  </div>
244
254
  </div>
245
255
 
@@ -253,4 +263,8 @@ watch(() => props.open, (val) => {
253
263
  <style scoped>
254
264
  .ai-insight-panel { min-height: 200px; }
255
265
  .ai-insight-panel :deep(.prose) { word-break: break-word; }
266
+ .ai-insight-panel :deep(.prose) h1 { font-size: 1rem; font-weight: 600; }
267
+ .ai-insight-panel :deep(.prose) h2 { font-size: 0.9375rem; font-weight: 600; }
268
+ .ai-insight-panel :deep(.prose) h3 { font-size: 0.875rem; font-weight: 600; }
269
+ .ai-insight-panel :deep(.prose) h4 { font-size: 0.8125rem; font-weight: 600; }
256
270
  </style>
@@ -0,0 +1,24 @@
1
+ export const messages = {
2
+ ru: {
3
+ aiInsight: {
4
+ title: 'AI анализ',
5
+ analyzing: 'Анализирую данные...',
6
+ noData: 'Нет данных для анализа',
7
+ askQuestion: 'Задать вопрос ИИ...',
8
+ retry: 'Повторить',
9
+ requestError: 'Ошибка запроса ({status})',
10
+ unknownError: 'Неизвестная ошибка'
11
+ }
12
+ },
13
+ en: {
14
+ aiInsight: {
15
+ title: 'AI Analysis',
16
+ analyzing: 'Analyzing data...',
17
+ noData: 'No data to analyze',
18
+ askQuestion: 'Ask the AI...',
19
+ retry: 'Retry',
20
+ requestError: 'Request failed ({status})',
21
+ unknownError: 'Unknown error'
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,12 @@
1
+ import { defineNuxtPlugin } from '#imports'
2
+ import { messages } from '../i18n/messages'
3
+
4
+ export default defineNuxtPlugin((nuxtApp) => {
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ const i18n: any = nuxtApp.vueApp.config.globalProperties.$i18n
7
+ if (i18n?.mergeLocaleMessage) {
8
+ for (const [locale, msg] of Object.entries(messages)) {
9
+ i18n.mergeLocaleMessage(locale, msg)
10
+ }
11
+ }
12
+ })
@@ -0,0 +1,18 @@
1
+ const messages = {
2
+ ru: {
3
+ CONFIG_ENDPOINT_MISSING: 'Не указан endpoint AI Insight',
4
+ CONFIG_KEY_MISSING: 'Не указан API-ключ AI Insight',
5
+ BACKEND_UNREACHABLE: 'Не удалось подключиться к AI Insight',
6
+ BACKEND_ERROR: 'Ошибка AI Insight'
7
+ },
8
+ en: {
9
+ CONFIG_ENDPOINT_MISSING: 'AI Insight endpoint is not configured',
10
+ CONFIG_KEY_MISSING: 'AI Insight API key is not configured',
11
+ BACKEND_UNREACHABLE: 'Failed to reach AI Insight backend',
12
+ BACKEND_ERROR: 'AI Insight error'
13
+ }
14
+ }
15
+
16
+ export function tError(key, lang) {
17
+ return messages[lang === 'en' ? 'en' : 'ru']?.[key] || key
18
+ }
@@ -1,18 +1,55 @@
1
- import { defineEventHandler, readBody, setHeaders, useRuntimeConfig } from 'h3'
1
+ import { tError } from './_errors.js'
2
2
 
3
3
  export default defineEventHandler(async (event) => {
4
4
  const config = useRuntimeConfig(event)
5
5
  const body = await readBody(event)
6
6
 
7
- const response = await $fetch.raw(config.aiInsight.endpoint, {
8
- method: 'POST',
9
- headers: {
10
- Authorization: `Bearer ${config.aiInsight.key}`,
11
- 'Content-Type': 'application/json'
12
- },
13
- body,
14
- responseType: 'stream'
15
- })
7
+ const endpoint = config.aiInsight.endpoint
8
+ const hasKey = !!config.aiInsight.key
9
+ const lang = getHeader(event, 'accept-language')?.slice(0, 2) || 'ru'
10
+
11
+ if (!endpoint) {
12
+ throw createError({ statusCode: 500, statusMessage: tError('CONFIG_ENDPOINT_MISSING', lang) })
13
+ }
14
+ if (!hasKey) {
15
+ throw createError({ statusCode: 500, statusMessage: tError('CONFIG_KEY_MISSING', lang) })
16
+ }
17
+
18
+ console.log('[ai-insight/proxy] →', endpoint, 'key:', hasKey ? 'yes' : 'NO KEY', 'data:', typeof body?.data, 'prompt:', body?.prompt?.slice(0, 80))
19
+
20
+ const origin = getHeader(event, 'origin') || getHeader(event, 'referer') || ''
21
+
22
+ let response
23
+ try {
24
+ response = await $fetch.raw(endpoint, {
25
+ method: 'POST',
26
+ headers: {
27
+ Authorization: `Bearer ${config.aiInsight.key}`,
28
+ 'Content-Type': 'application/json',
29
+ ...(origin ? { Origin: origin } : {})
30
+ },
31
+ body,
32
+ responseType: 'stream',
33
+ ignoreResponseError: true
34
+ })
35
+ } catch (fetchErr) {
36
+ console.error('[ai-insight/proxy] fetch error:', fetchErr)
37
+ throw createError({ statusCode: 502, statusMessage: tError('BACKEND_UNREACHABLE', lang) })
38
+ }
39
+
40
+ console.log('[ai-insight/proxy] ←', response.status, response.statusText)
41
+
42
+ if (!response.ok) {
43
+ const text = await response.text().catch(() => '')
44
+ let detail
45
+ try {
46
+ const json = JSON.parse(text)
47
+ detail = json.message || json.statusMessage || text
48
+ } catch {
49
+ detail = text || response.statusText
50
+ }
51
+ throw createError({ statusCode: response.status, statusMessage: detail || tError('BACKEND_ERROR', lang) })
52
+ }
16
53
 
17
54
  setHeaders(event, {
18
55
  'Content-Type': 'text/event-stream',
@@ -0,0 +1,29 @@
1
+ export async function* parseUIMessageStream(response: Response) {
2
+ const reader = response.body!.getReader()
3
+ const decoder = new TextDecoder()
4
+ let buffer = ''
5
+
6
+ while (true) {
7
+ const { done, value } = await reader.read()
8
+ if (done) break
9
+
10
+ buffer += decoder.decode(value, { stream: true })
11
+ const lines = buffer.split('\n')
12
+ buffer = lines.pop() || ''
13
+
14
+ for (const line of lines) {
15
+ if (line.startsWith('data: ')) {
16
+ const data = line.slice(6).trim()
17
+ if (data === '[DONE]') return
18
+ try {
19
+ const chunk = JSON.parse(data)
20
+ if (chunk.type === 'text-delta' && chunk.delta != null) {
21
+ yield chunk.delta
22
+ }
23
+ } catch {
24
+ console.warn('[parseUIMessageStream] parse error:', data)
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mirodan/ai-insight",
3
- "version": "0.1.3",
4
- "description": "AI-powered insight tooltip component for Nuxt",
3
+ "version": "0.1.5",
4
+ "description": "AI-powered insight tooltip component for Mirodan",
5
5
  "type": "module",
6
6
  "main": "./dist/module.js",
7
7
  "types": "./dist/module.d.ts",
@@ -35,7 +35,8 @@
35
35
  "typescript": "^5.9.3",
36
36
  "typescript-eslint": "^8.65.0",
37
37
  "vite": "^8.2.0",
38
- "vitest": "^3.1.2"
38
+ "vitest": "^3.1.2",
39
+ "vue-i18n": "^11.4.6"
39
40
  },
40
41
  "peerDependencies": {
41
42
  "@comark/nuxt": "^0.4.0",
@@ -44,6 +45,7 @@
44
45
  "@nuxt/ui": "^4.0.0",
45
46
  "nuxt": "^4.0.0",
46
47
  "tailwindcss": "^4.0.0",
47
- "vue": "^3.0.0"
48
+ "vue": "^3.0.0",
49
+ "vue-i18n": "^11.0.0"
48
50
  }
49
51
  }