inicontent 1.0.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/.github/pull_request.yml +18 -0
- package/CONTRIBUTING.md +67 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/app/app.vue +106 -0
- package/app/assets/main.css +974 -0
- package/app/components/Api/EndpointCard.vue +169 -0
- package/app/components/Api/SchemaDocsTable.vue +208 -0
- package/app/components/Asset/Card.vue +1363 -0
- package/app/components/Asset/Grid.vue +295 -0
- package/app/components/Asset/Icon.vue +38 -0
- package/app/components/Asset/Preview.vue +486 -0
- package/app/components/Asset/Scanner.vue +700 -0
- package/app/components/Asset/Thumb.vue +708 -0
- package/app/components/Auth/Reset.vue +99 -0
- package/app/components/Auth/index.vue +346 -0
- package/app/components/ChatInterface.vue +2326 -0
- package/app/components/ChatPagePreview.vue +34 -0
- package/app/components/Column/Array.vue +29 -0
- package/app/components/Column/Asset.vue +21 -0
- package/app/components/Column/Boolean.vue +12 -0
- package/app/components/Column/Color.vue +9 -0
- package/app/components/Column/Date.vue +57 -0
- package/app/components/Column/Edit.vue +215 -0
- package/app/components/Column/Email.vue +9 -0
- package/app/components/Column/Html.vue +27 -0
- package/app/components/Column/Id.vue +20 -0
- package/app/components/Column/Locale.vue +32 -0
- package/app/components/Column/Object.vue +18 -0
- package/app/components/Column/Password.vue +7 -0
- package/app/components/Column/Role.vue +15 -0
- package/app/components/Column/S.vue +14 -0
- package/app/components/Column/Table/Single.vue +49 -0
- package/app/components/Column/Table/index.vue +42 -0
- package/app/components/Column/Tags.vue +31 -0
- package/app/components/Column/Text.vue +9 -0
- package/app/components/Column/Url.vue +9 -0
- package/app/components/Column/index.vue +41 -0
- package/app/components/Dashboard/Editor.vue +221 -0
- package/app/components/Dashboard/Grid.vue +270 -0
- package/app/components/Dashboard/View.vue +131 -0
- package/app/components/Dashboard/Widget/BarChart.vue +73 -0
- package/app/components/Dashboard/Widget/Counter.vue +51 -0
- package/app/components/Dashboard/Widget/LineChart.vue +76 -0
- package/app/components/Dashboard/Widget/PieChart.vue +75 -0
- package/app/components/Dashboard/Widget/Table.vue +94 -0
- package/app/components/Dashboard/WidgetEditor.vue +274 -0
- package/app/components/Data/Array.vue +102 -0
- package/app/components/Data/Html.vue +9 -0
- package/app/components/Data/Icon.vue +9 -0
- package/app/components/Data/Object.vue +10 -0
- package/app/components/Data/S.vue +16 -0
- package/app/components/Data/Table.vue +53 -0
- package/app/components/Data/Value.vue +34 -0
- package/app/components/Data/index.vue +14 -0
- package/app/components/Field/Array.vue +324 -0
- package/app/components/Field/Asset/Actions.vue +97 -0
- package/app/components/Field/Asset/index.vue +639 -0
- package/app/components/Field/Boolean.vue +26 -0
- package/app/components/Field/Checkbox.vue +48 -0
- package/app/components/Field/Color.vue +27 -0
- package/app/components/Field/Date.vue +30 -0
- package/app/components/Field/Email.vue +51 -0
- package/app/components/Field/EmailTemplate.vue +420 -0
- package/app/components/Field/Html/ColorPicker.vue +127 -0
- package/app/components/Field/Html/Editor.vue +544 -0
- package/app/components/Field/Html/index.vue +25 -0
- package/app/components/Field/Icon.vue +114 -0
- package/app/components/Field/JSON.vue +113 -0
- package/app/components/Field/Mention.vue +36 -0
- package/app/components/Field/Number.vue +27 -0
- package/app/components/Field/Object.vue +92 -0
- package/app/components/Field/Password.vue +55 -0
- package/app/components/Field/Radio.vue +47 -0
- package/app/components/Field/S.vue +44 -0
- package/app/components/Field/Select.vue +49 -0
- package/app/components/Field/Table.vue +348 -0
- package/app/components/Field/Tags.vue +113 -0
- package/app/components/Field/Text.vue +31 -0
- package/app/components/Field/Textarea.vue +33 -0
- package/app/components/Field/Url.vue +40 -0
- package/app/components/Field/Wrapper.vue +128 -0
- package/app/components/Field/index.vue +68 -0
- package/app/components/FloatingChatbot.vue +73 -0
- package/app/components/Form/Card.vue +151 -0
- package/app/components/Form/Drawer.vue +195 -0
- package/app/components/Form/index.vue +621 -0
- package/app/components/Header.vue +328 -0
- package/app/components/Offline/ConflictModal.vue +200 -0
- package/app/components/Offline/SyncStatus.vue +249 -0
- package/app/components/PlatformFeatureLanding.vue +65 -0
- package/app/components/Table/Backups.vue +638 -0
- package/app/components/Table/Flows.vue +814 -0
- package/app/components/Table/Grid.vue +296 -0
- package/app/components/Table/Icon.vue +36 -0
- package/app/components/Table/Logs.vue +181 -0
- package/app/components/Table/Schedules.vue +722 -0
- package/app/components/Table/Search/Button.vue +256 -0
- package/app/components/Table/Search/Items.vue +286 -0
- package/app/components/Table/Search/index.vue +87 -0
- package/app/components/Table/Settings/Schema.vue +801 -0
- package/app/components/Table/Settings/index.vue +631 -0
- package/app/components/Table/TranslateDrawer.vue +526 -0
- package/app/components/Table/Views/Kanban.vue +289 -0
- package/app/components/Table/Views/Table.vue +1273 -0
- package/app/components/Table/index.vue +890 -0
- package/app/composables/Translation/define.ts +41 -0
- package/app/composables/Translation/fetch.ts +107 -0
- package/app/composables/Translation/languages.ts +1 -0
- package/app/composables/Translation/loadCoreTranslations.ts +38 -0
- package/app/composables/Translation/translate.ts +250 -0
- package/app/composables/databaseCookies.ts +79 -0
- package/app/composables/fieldValidator.ts +72 -0
- package/app/composables/fieldsList.ts +301 -0
- package/app/composables/formatDatabase.ts +101 -0
- package/app/composables/index.ts +444 -0
- package/app/composables/openDrawer.ts +118 -0
- package/app/composables/optimizeFile.ts +287 -0
- package/app/composables/renderLabel.ts +88 -0
- package/app/composables/schemaClipboard.ts +95 -0
- package/app/composables/search.ts +353 -0
- package/app/composables/setThemeConfig.ts +48 -0
- package/app/composables/translationValue.ts +169 -0
- package/app/composables/useAssetPreview.ts +84 -0
- package/app/composables/useAssetUploader.ts +56 -0
- package/app/composables/useDashboardData.ts +310 -0
- package/app/composables/useDocumentThumbnail.ts +351 -0
- package/app/composables/useEcharts.ts +33 -0
- package/app/composables/useLocalDatabaseConfig.ts +51 -0
- package/app/composables/useOcr.ts +300 -0
- package/app/composables/useOfflineFetch.ts +194 -0
- package/app/composables/useOfflineItem.ts +107 -0
- package/app/composables/useOfflineQueue.ts +302 -0
- package/app/composables/useOfflineSync.ts +235 -0
- package/app/composables/usePasskeyAuth.ts +156 -0
- package/app/composables/usePdfCompressor.ts +180 -0
- package/app/composables/usePlatformRedirect.ts +72 -0
- package/app/composables/useRealtimeSync.ts +233 -0
- package/app/composables/useReferencedItems.ts +180 -0
- package/app/composables/useScanner.ts +664 -0
- package/app/composables/useSubscription.ts +650 -0
- package/app/composables/useThumbnailCache.ts +136 -0
- package/app/composables/useVideoCompressor.ts +194 -0
- package/app/error.vue +112 -0
- package/app/layouts/api.vue +188 -0
- package/app/layouts/dashboard.vue +91 -0
- package/app/layouts/default.vue +228 -0
- package/app/layouts/table.vue +471 -0
- package/app/locales/ar.ts +996 -0
- package/app/locales/en.ts +655 -0
- package/app/locales/es.ts +996 -0
- package/app/locales/fr.ts +997 -0
- package/app/middleware/dashboard.ts +33 -0
- package/app/middleware/database.ts +83 -0
- package/app/middleware/global.ts +1 -0
- package/app/middleware/sid-ingest.global.ts +29 -0
- package/app/middleware/table.ts +54 -0
- package/app/middleware/user.ts +26 -0
- package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
- package/app/pages/[[database]]/admin/api/index.vue +18 -0
- package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
- package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
- package/app/pages/[[database]]/admin/billing/index.vue +16 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
- package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
- package/app/pages/[[database]]/admin/index.vue +28 -0
- package/app/pages/[[database]]/admin/settings.vue +526 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
- package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
- package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
- package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
- package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/index.vue +20 -0
- package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
- package/app/pages/[[database]]/api/index.vue +120 -0
- package/app/pages/[[database]]/auth/index.vue +12 -0
- package/app/pages/[[database]]/auth/reset.vue +10 -0
- package/app/pages/[[database]]/index.vue +10 -0
- package/app/plugins/offline-warmup.client.ts +53 -0
- package/app/plugins/template-globals.ts +9 -0
- package/app/spa-loading-template.html +93 -0
- package/biome.json +53 -0
- package/index.d.ts +380 -0
- package/modules/naiveui.ts +41 -0
- package/nuxt.config.ts +269 -0
- package/package.json +56 -0
- package/pnpm-workspace.yaml +5 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/pwa-192x192.png +0 -0
- package/public/pwa-512x512.png +0 -0
- package/public/pwa-maskable-192x192.png +0 -0
- package/public/pwa-maskable-512x512.png +0 -0
- package/public/vendor/opencv.js +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,2326 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="chat-interface">
|
|
3
|
+
<template v-if="subscriptionChecked && hasActiveSubscription">
|
|
4
|
+
<div ref="scrollbarRef" class="messages-area" @scroll="handleScroll">
|
|
5
|
+
<NFlex v-for="(message, index) in messages" :key="index" :wrap="false" align="start" class="message-row"
|
|
6
|
+
:justify="isRTL ? (message.sender === 'User' ? 'start' : 'end') : (message.sender === 'User' ? 'end' : 'start')"
|
|
7
|
+
:style="{ flexDirection: isRTL ? 'row-reverse' : 'row' }">
|
|
8
|
+
<NButton v-if="message.sender === 'AI'" circle>
|
|
9
|
+
<Icon name="tabler:robot" />
|
|
10
|
+
</NButton>
|
|
11
|
+
|
|
12
|
+
<NCard :bordered="true" class="message-card">
|
|
13
|
+
<template
|
|
14
|
+
#header
|
|
15
|
+
v-if="message.action && (['database_approval_pending', 'roles_defined', 'tables_naming_pending', 'tables_approval_pending', 'structure_generated', 'data_approval_pending', 'translation_approval_pending']).includes(message.action)"
|
|
16
|
+
>
|
|
17
|
+
<NFlex align="center" :wrap="true">
|
|
18
|
+
<NTag v-if="message.action === 'database_approval_pending' && getMessageDatabasePlan(message)" size="small" round type="success">
|
|
19
|
+
<template #icon><NIcon><Icon name="tabler:database-plus" /></NIcon></template>
|
|
20
|
+
{{ getMessageDatabasePlan(message)?.slug }}
|
|
21
|
+
</NTag>
|
|
22
|
+
|
|
23
|
+
<NButton
|
|
24
|
+
v-else-if="message.action === 'tables_naming_pending'"
|
|
25
|
+
v-for="(tableName, nameIndex) in getMessageNameTables(message)"
|
|
26
|
+
:key="nameIndex"
|
|
27
|
+
size="small"
|
|
28
|
+
round
|
|
29
|
+
:title="tableName.label || tableName.slug"
|
|
30
|
+
>
|
|
31
|
+
<template #icon>
|
|
32
|
+
<LazyTableIcon :table="tableName" />
|
|
33
|
+
</template>
|
|
34
|
+
{{ tableName.slug }}
|
|
35
|
+
<NTag
|
|
36
|
+
v-if="tableName.isNew !== undefined"
|
|
37
|
+
:type="tableName.isNew ? 'success' : 'warning'"
|
|
38
|
+
size="small"
|
|
39
|
+
round
|
|
40
|
+
style="margin-inline-start: 4px;"
|
|
41
|
+
>
|
|
42
|
+
{{ tableName.isNew ? t('new') : t('edit') }}
|
|
43
|
+
</NTag>
|
|
44
|
+
</NButton>
|
|
45
|
+
|
|
46
|
+
<NButton
|
|
47
|
+
v-else-if="message.action === 'tables_approval_pending'"
|
|
48
|
+
v-for="(table, tableIndex) in getMessageTables(message)"
|
|
49
|
+
:key="tableIndex"
|
|
50
|
+
size="small"
|
|
51
|
+
round
|
|
52
|
+
@click="openTableDemoModal(table)"
|
|
53
|
+
>
|
|
54
|
+
<template #icon>
|
|
55
|
+
<LazyTableIcon :table />
|
|
56
|
+
</template>
|
|
57
|
+
{{ table.slug }}
|
|
58
|
+
<NTag
|
|
59
|
+
v-if="table.isNew !== undefined"
|
|
60
|
+
:type="table.isNew ? 'success' : 'warning'"
|
|
61
|
+
size="small"
|
|
62
|
+
round
|
|
63
|
+
style="margin-inline-start: 4px;"
|
|
64
|
+
>
|
|
65
|
+
{{ table.isNew ? t('new') : t('edit') }}
|
|
66
|
+
</NTag>
|
|
67
|
+
</NButton>
|
|
68
|
+
|
|
69
|
+
<NButton
|
|
70
|
+
v-else-if="message.action === 'roles_defined'"
|
|
71
|
+
v-for="table in uniqueTables"
|
|
72
|
+
:key="table"
|
|
73
|
+
@click="selectRoleTable(table)"
|
|
74
|
+
:type="selectedTable === table ? 'primary' : 'default'"
|
|
75
|
+
size="small"
|
|
76
|
+
round
|
|
77
|
+
>
|
|
78
|
+
<template #icon>
|
|
79
|
+
<Icon :name="`tabler:${getTableIcon(table) || 'question-mark'}`" />
|
|
80
|
+
</template>
|
|
81
|
+
{{ table }}
|
|
82
|
+
</NButton>
|
|
83
|
+
|
|
84
|
+
<template v-else-if="message.action === 'structure_generated'">
|
|
85
|
+
<NButton
|
|
86
|
+
v-for="page in getMessagePages(message)"
|
|
87
|
+
:key="page.slug"
|
|
88
|
+
size="small"
|
|
89
|
+
round
|
|
90
|
+
@click="openPagePreviewModal(page)"
|
|
91
|
+
>
|
|
92
|
+
/{{ page.slug }}
|
|
93
|
+
</NButton>
|
|
94
|
+
<NButton
|
|
95
|
+
v-for="(rb, rbIndex) in getMessageReusableBlocks(message)"
|
|
96
|
+
:key="`reusable-${rbIndex}`"
|
|
97
|
+
size="small"
|
|
98
|
+
round
|
|
99
|
+
type="warning"
|
|
100
|
+
>
|
|
101
|
+
<template #icon>
|
|
102
|
+
<NIcon><Icon name="tabler:layers-linked" /></NIcon>
|
|
103
|
+
</template>
|
|
104
|
+
{{ rb.name }}
|
|
105
|
+
</NButton>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<template v-else-if="message.action === 'data_approval_pending'">
|
|
109
|
+
<NButton
|
|
110
|
+
v-for="item in getMessageDataItems(message)"
|
|
111
|
+
:key="item.table"
|
|
112
|
+
size="small"
|
|
113
|
+
round
|
|
114
|
+
type="success"
|
|
115
|
+
secondary
|
|
116
|
+
@click="openDataItemPreviewModal(item)"
|
|
117
|
+
>
|
|
118
|
+
<template #icon><NIcon><Icon name="tabler:eye" /></NIcon></template>
|
|
119
|
+
{{ item.table }} ({{ item.records.length }})
|
|
120
|
+
</NButton>
|
|
121
|
+
<NTag v-if="message.loadingMore" size="small" round type="warning">
|
|
122
|
+
<template #icon><NIcon><Icon name="tabler:loader-2" class="spin-icon" /></NIcon></template>
|
|
123
|
+
{{ t('generatingMoreTables') }}
|
|
124
|
+
</NTag>
|
|
125
|
+
</template>
|
|
126
|
+
|
|
127
|
+
<template v-else-if="message.action === 'translation_approval_pending'">
|
|
128
|
+
<NTag v-for="item in getMessageTranslationPlan(message)" :key="`${item.table}_${item.locale}`" size="small" round type="warning">
|
|
129
|
+
<template #icon><NIcon><Icon name="tabler:language" /></NIcon></template>
|
|
130
|
+
{{ item.table }} → {{ item.locale }}
|
|
131
|
+
</NTag>
|
|
132
|
+
</template>
|
|
133
|
+
</NFlex>
|
|
134
|
+
|
|
135
|
+
<NFlex v-if="message.action === 'database_approval_pending' && !message.applied && isAdmin" justify="end" style="margin-top: 8px;">
|
|
136
|
+
<NButton size="small" type="primary" :loading="applyingDatabaseIndex === index" :disabled="applyingDatabaseIndex !== null" @click="applyDatabase(message, index)">
|
|
137
|
+
<template #icon>
|
|
138
|
+
<NIcon>
|
|
139
|
+
<Icon name="tabler:database-plus" />
|
|
140
|
+
</NIcon>
|
|
141
|
+
</template>
|
|
142
|
+
{{ t('createDatabase') }}
|
|
143
|
+
</NButton>
|
|
144
|
+
</NFlex>
|
|
145
|
+
<NFlex v-else-if="message.action === 'database_approval_pending' && message.applied" justify="end" style="margin-top: 8px;">
|
|
146
|
+
<NTag type="success" size="small" round>
|
|
147
|
+
<template #icon>
|
|
148
|
+
<NIcon>
|
|
149
|
+
<Icon name="tabler:check" />
|
|
150
|
+
</NIcon>
|
|
151
|
+
</template>
|
|
152
|
+
{{ t('applied') }}
|
|
153
|
+
</NTag>
|
|
154
|
+
</NFlex>
|
|
155
|
+
|
|
156
|
+
<NFlex v-if="message.action === 'tables_approval_pending' && !message.applied && isAdmin" justify="end" style="margin-top: 8px;">
|
|
157
|
+
<NButton size="small" type="primary" :loading="applyingIndex === index" :disabled="applyingIndex !== null" @click="applyTables(message, index)">
|
|
158
|
+
<template #icon>
|
|
159
|
+
<NIcon>
|
|
160
|
+
<Icon name="tabler:check" />
|
|
161
|
+
</NIcon>
|
|
162
|
+
</template>
|
|
163
|
+
{{ t('applyChanges') }}
|
|
164
|
+
</NButton>
|
|
165
|
+
</NFlex>
|
|
166
|
+
<NFlex v-else-if="message.action === 'tables_approval_pending' && message.applied" justify="end" style="margin-top: 8px;">
|
|
167
|
+
<NTag type="success" size="small" round>
|
|
168
|
+
<template #icon>
|
|
169
|
+
<NIcon>
|
|
170
|
+
<Icon name="tabler:check" />
|
|
171
|
+
</NIcon>
|
|
172
|
+
</template>
|
|
173
|
+
{{ t('applied') }}
|
|
174
|
+
</NTag>
|
|
175
|
+
</NFlex>
|
|
176
|
+
|
|
177
|
+
<NFlex v-if="message.action === 'tables_naming_pending' && !message.continued && !message.superseded && isAdmin" justify="end" style="margin-top: 8px;">
|
|
178
|
+
<NButton size="small" type="primary" :loading="applyingIndex === index" :disabled="applyingIndex !== null" @click="continueToSchema(message, index)">
|
|
179
|
+
<template #icon>
|
|
180
|
+
<NIcon>
|
|
181
|
+
<Icon name="tabler:arrow-right" />
|
|
182
|
+
</NIcon>
|
|
183
|
+
</template>
|
|
184
|
+
{{ t('continue') }}
|
|
185
|
+
</NButton>
|
|
186
|
+
</NFlex>
|
|
187
|
+
<NFlex v-else-if="message.action === 'tables_naming_pending' && message.superseded" justify="end" style="margin-top: 8px;">
|
|
188
|
+
<NTag type="info" size="small" round>
|
|
189
|
+
<template #icon>
|
|
190
|
+
<NIcon>
|
|
191
|
+
<Icon name="tabler:refresh" />
|
|
192
|
+
</NIcon>
|
|
193
|
+
</template>
|
|
194
|
+
{{ t('namesUpdated') }}
|
|
195
|
+
</NTag>
|
|
196
|
+
</NFlex>
|
|
197
|
+
<NFlex v-else-if="message.action === 'tables_naming_pending' && message.continued" justify="end" style="margin-top: 8px;">
|
|
198
|
+
<NTag type="success" size="small" round>
|
|
199
|
+
<template #icon>
|
|
200
|
+
<NIcon>
|
|
201
|
+
<Icon name="tabler:check" />
|
|
202
|
+
</NIcon>
|
|
203
|
+
</template>
|
|
204
|
+
{{ t('continued') }}
|
|
205
|
+
</NTag>
|
|
206
|
+
</NFlex>
|
|
207
|
+
|
|
208
|
+
<NFlex v-if="message.action === 'data_approval_pending' && !message.applied && isAdmin" justify="end" style="margin-top: 8px;">
|
|
209
|
+
<NButton size="small" type="primary" :loading="applyingDataIndex === index" :disabled="applyingDataIndex !== null || message.loadingMore" @click="applyData(message, index)">
|
|
210
|
+
<template #icon>
|
|
211
|
+
<NIcon>
|
|
212
|
+
<Icon name="tabler:database-import" />
|
|
213
|
+
</NIcon>
|
|
214
|
+
</template>
|
|
215
|
+
{{ t('publishData') }}
|
|
216
|
+
</NButton>
|
|
217
|
+
</NFlex>
|
|
218
|
+
<NFlex v-else-if="message.action === 'data_approval_pending' && message.applied" justify="end" style="margin-top: 8px;">
|
|
219
|
+
<NTag type="success" size="small" round>
|
|
220
|
+
<template #icon>
|
|
221
|
+
<NIcon>
|
|
222
|
+
<Icon name="tabler:check" />
|
|
223
|
+
</NIcon>
|
|
224
|
+
</template>
|
|
225
|
+
{{ t('published') }}
|
|
226
|
+
</NTag>
|
|
227
|
+
</NFlex>
|
|
228
|
+
|
|
229
|
+
<NFlex v-if="message.action === 'translation_approval_pending' && !message.applied && isAdmin" justify="end" style="margin-top: 8px;">
|
|
230
|
+
<NButton size="small" type="primary" :loading="applyingTranslateIndex === index" :disabled="applyingTranslateIndex !== null" @click="applyTranslation(message, index)">
|
|
231
|
+
<template #icon>
|
|
232
|
+
<NIcon>
|
|
233
|
+
<Icon name="tabler:language" />
|
|
234
|
+
</NIcon>
|
|
235
|
+
</template>
|
|
236
|
+
{{ t('translateData') }}
|
|
237
|
+
</NButton>
|
|
238
|
+
</NFlex>
|
|
239
|
+
<NFlex v-else-if="message.action === 'translation_approval_pending' && message.applied" justify="end" style="margin-top: 8px;">
|
|
240
|
+
<NTag type="success" size="small" round>
|
|
241
|
+
<template #icon>
|
|
242
|
+
<NIcon>
|
|
243
|
+
<Icon name="tabler:check" />
|
|
244
|
+
</NIcon>
|
|
245
|
+
</template>
|
|
246
|
+
{{ t('translated') }}
|
|
247
|
+
</NTag>
|
|
248
|
+
</NFlex>
|
|
249
|
+
</template>
|
|
250
|
+
|
|
251
|
+
<template v-if="message.action === 'roles_defined' && selectedTable">
|
|
252
|
+
<NTable striped size="small" style="margin-top: 8px;">
|
|
253
|
+
<thead>
|
|
254
|
+
<tr>
|
|
255
|
+
<th>{{ t('role') }}</th>
|
|
256
|
+
<th>C</th>
|
|
257
|
+
<th>R</th>
|
|
258
|
+
<th>U</th>
|
|
259
|
+
<th>D</th>
|
|
260
|
+
</tr>
|
|
261
|
+
</thead>
|
|
262
|
+
<tbody>
|
|
263
|
+
<tr v-for="roleData in rolesForSelectedTable" :key="roleData.role">
|
|
264
|
+
<td>{{ roleData.role }}</td>
|
|
265
|
+
<td>
|
|
266
|
+
<NIcon :color="getPermissionIcon(roleData.allowedMethods, 'c').color">
|
|
267
|
+
<Icon :name="getPermissionIcon(roleData.allowedMethods, 'c').name" />
|
|
268
|
+
</NIcon>
|
|
269
|
+
</td>
|
|
270
|
+
<td>
|
|
271
|
+
<NIcon :color="getPermissionIcon(roleData.allowedMethods, 'r').color">
|
|
272
|
+
<Icon :name="getPermissionIcon(roleData.allowedMethods, 'r').name" />
|
|
273
|
+
</NIcon>
|
|
274
|
+
</td>
|
|
275
|
+
<td>
|
|
276
|
+
<NIcon :color="getPermissionIcon(roleData.allowedMethods, 'u').color">
|
|
277
|
+
<Icon :name="getPermissionIcon(roleData.allowedMethods, 'u').name" />
|
|
278
|
+
</NIcon>
|
|
279
|
+
</td>
|
|
280
|
+
<td>
|
|
281
|
+
<NIcon :color="getPermissionIcon(roleData.allowedMethods, 'd').color">
|
|
282
|
+
<Icon :name="getPermissionIcon(roleData.allowedMethods, 'd').name" />
|
|
283
|
+
</NIcon>
|
|
284
|
+
</td>
|
|
285
|
+
</tr>
|
|
286
|
+
</tbody>
|
|
287
|
+
</NTable>
|
|
288
|
+
</template>
|
|
289
|
+
|
|
290
|
+
<div class="message-text">
|
|
291
|
+
{{ message.text }}
|
|
292
|
+
</div>
|
|
293
|
+
<NFlex
|
|
294
|
+
v-if="message.translationResults?.length"
|
|
295
|
+
vertical
|
|
296
|
+
:size="8"
|
|
297
|
+
class="translation-results"
|
|
298
|
+
>
|
|
299
|
+
<NFlex
|
|
300
|
+
v-for="result in message.translationResults"
|
|
301
|
+
:key="`${result.table}_${result.locale}`"
|
|
302
|
+
align="center"
|
|
303
|
+
justify="space-between"
|
|
304
|
+
:wrap="false"
|
|
305
|
+
class="translation-result-row"
|
|
306
|
+
>
|
|
307
|
+
<NFlex align="center" :size="8" :wrap="false">
|
|
308
|
+
<strong>{{ result.table }}</strong>
|
|
309
|
+
<NTag size="small" round type="info">{{ result.locale }}</NTag>
|
|
310
|
+
</NFlex>
|
|
311
|
+
<span class="translation-result-count">
|
|
312
|
+
{{ t('translationCount', { count: result.count }) }}
|
|
313
|
+
</span>
|
|
314
|
+
</NFlex>
|
|
315
|
+
</NFlex>
|
|
316
|
+
</NCard>
|
|
317
|
+
|
|
318
|
+
<NButton v-if="message.sender === 'User'" type="primary" circle>
|
|
319
|
+
<Icon name="tabler:user-circle" />
|
|
320
|
+
</NButton>
|
|
321
|
+
</NFlex>
|
|
322
|
+
</div>
|
|
323
|
+
<NFlex align="center" class="message-input">
|
|
324
|
+
<NInput ref="inputRef" type="textarea" :rows="1" style="flex: 1; max-height: 200px; overflow-y: auto"
|
|
325
|
+
:autosize="{ minRows: 2, maxRows: 5 }" v-model:value="currentMessage" :placeholder="dynamicPlaceholder"
|
|
326
|
+
@keyup.enter="handleEnterKey" :disabled="loading" clearable />
|
|
327
|
+
<NButton :loading type="primary" secondary @click="sendMessage"
|
|
328
|
+
:disabled="!currentMessage.trim() || loading">
|
|
329
|
+
<template #icon>
|
|
330
|
+
<NIcon>
|
|
331
|
+
<Icon name="tabler:send" />
|
|
332
|
+
</NIcon>
|
|
333
|
+
</template>
|
|
334
|
+
</NButton>
|
|
335
|
+
</NFlex>
|
|
336
|
+
<NFlex v-if="$attrs.onSkip" justify="center">
|
|
337
|
+
<NButton text size="small" :disabled="loading" @click="$emit('skip')">
|
|
338
|
+
{{ t('skipAiHelp') }}
|
|
339
|
+
</NButton>
|
|
340
|
+
</NFlex>
|
|
341
|
+
<NModal v-model:show="showTableModal" preset="card" :title="`${t(previewModalTitleKey)}: ${t(dataTable.title)}`"
|
|
342
|
+
size="large">
|
|
343
|
+
<template #header-extra>
|
|
344
|
+
<NButton v-if="tableModalHistory.length" size="small" secondary circle @click="undoTableDemoModal">
|
|
345
|
+
<template #icon>
|
|
346
|
+
<NIcon><Icon name="tabler:arrow-back-up" /></NIcon>
|
|
347
|
+
</template>
|
|
348
|
+
</NButton>
|
|
349
|
+
</template>
|
|
350
|
+
<NDataTable v-if="showTableModal" :columns="dataTable.columns" :data="dataTable.data" />
|
|
351
|
+
</NModal>
|
|
352
|
+
|
|
353
|
+
<LazyChatPagePreview
|
|
354
|
+
v-if="showPageModal"
|
|
355
|
+
:show="showPageModal"
|
|
356
|
+
:page="selectedPageData"
|
|
357
|
+
:primary-color="databaseModel?.primaryColor"
|
|
358
|
+
@update:show="showPageModal = $event"
|
|
359
|
+
/>
|
|
360
|
+
</template>
|
|
361
|
+
|
|
362
|
+
<template v-else-if="subscriptionChecked">
|
|
363
|
+
<NCard :bordered="true" class="message-card" style="max-width: 100%;">
|
|
364
|
+
<NFlex vertical :size="12" align="start">
|
|
365
|
+
<div style="white-space: pre-wrap">
|
|
366
|
+
{{ t('chatPaidPlanRequired') }}
|
|
367
|
+
</div>
|
|
368
|
+
<NFlex :wrap="true" :size="8">
|
|
369
|
+
<NButton type="primary" secondary @click="goToPricing">
|
|
370
|
+
{{ t('billing') }}
|
|
371
|
+
</NButton>
|
|
372
|
+
<NButton v-if="$attrs.onSkip" text @click="$emit('skip')">
|
|
373
|
+
{{ t('skipAiHelp') }}
|
|
374
|
+
</NButton>
|
|
375
|
+
</NFlex>
|
|
376
|
+
</NFlex>
|
|
377
|
+
</NCard>
|
|
378
|
+
</template>
|
|
379
|
+
</div>
|
|
380
|
+
</template>
|
|
381
|
+
|
|
382
|
+
<script setup lang="ts">
|
|
383
|
+
import type { DataTableColumns } from "naive-ui";
|
|
384
|
+
import { Icon, LazyColumn, NFlex } from "#components";
|
|
385
|
+
|
|
386
|
+
const props = defineProps<{
|
|
387
|
+
endpoint?: string;
|
|
388
|
+
persistKey?: string;
|
|
389
|
+
resetNonce?: number;
|
|
390
|
+
}>();
|
|
391
|
+
const emit = defineEmits<{ skip: [] }>();
|
|
392
|
+
const databaseModel = defineModel<Database>("database");
|
|
393
|
+
const route = useRoute();
|
|
394
|
+
const { hasActiveSubscription, loadSubscriptionData } = useSubscription();
|
|
395
|
+
|
|
396
|
+
type MessageAction =
|
|
397
|
+
| "clarification_needed"
|
|
398
|
+
| "database_approval_pending"
|
|
399
|
+
| "tables_naming_pending"
|
|
400
|
+
| "tables_approval_pending"
|
|
401
|
+
| "data_approval_pending"
|
|
402
|
+
| "translation_approval_pending"
|
|
403
|
+
| "rejected"
|
|
404
|
+
| "error"
|
|
405
|
+
| "roles_defined"
|
|
406
|
+
| "redirect"
|
|
407
|
+
| "structure_generated";
|
|
408
|
+
|
|
409
|
+
type TableResponse = {
|
|
410
|
+
slug: string;
|
|
411
|
+
schema: Schema;
|
|
412
|
+
demo: Array<Record<string, unknown>>;
|
|
413
|
+
defaultSearchableColumns?: number[];
|
|
414
|
+
icon?: string;
|
|
415
|
+
isNew?: boolean;
|
|
416
|
+
label?: string;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/** A proposed table name (names step): no schema/demo yet. */
|
|
420
|
+
type TableNameProposal = {
|
|
421
|
+
id?: string;
|
|
422
|
+
slug: string;
|
|
423
|
+
oldSlug?: string;
|
|
424
|
+
label?: string;
|
|
425
|
+
icon?: string;
|
|
426
|
+
isNew?: boolean;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
type RolePermission = {
|
|
430
|
+
table: string;
|
|
431
|
+
allowedMethods: string;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
type RoleResponse = {
|
|
435
|
+
role: string;
|
|
436
|
+
permissions: RolePermission[];
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
type PageResponse = {
|
|
440
|
+
slug: string;
|
|
441
|
+
seo?: { title?: string };
|
|
442
|
+
template?: { table?: string; segments?: Record<string, string> };
|
|
443
|
+
content?: unknown;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
/** A NEW reusable component proposed by the AI (no id yet — created later). */
|
|
447
|
+
type ReusableBlockProposal = {
|
|
448
|
+
name: string;
|
|
449
|
+
config?: Record<string, unknown>;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
type DataItem = {
|
|
453
|
+
table: string;
|
|
454
|
+
records: Array<Record<string, unknown>>;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
type TranslationPlanItem = {
|
|
458
|
+
table: string;
|
|
459
|
+
locale: string;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
type TranslationResult = {
|
|
463
|
+
table: string;
|
|
464
|
+
locale: string;
|
|
465
|
+
count: number;
|
|
466
|
+
success: boolean;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
type DatabasePlan = {
|
|
470
|
+
slug: string;
|
|
471
|
+
primaryLanguage?: string;
|
|
472
|
+
secondaryLanguages?: string[];
|
|
473
|
+
primaryColor?: string;
|
|
474
|
+
tablesPrompt?: string;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
type AIResponsePayload = {
|
|
478
|
+
action: MessageAction | "redirect";
|
|
479
|
+
target?: string;
|
|
480
|
+
questions?: string[];
|
|
481
|
+
tables?: TableResponse[] | TableNameProposal[];
|
|
482
|
+
roles?: RoleResponse[];
|
|
483
|
+
pages?: PageResponse[];
|
|
484
|
+
items?: DataItem[] | TranslationPlanItem[];
|
|
485
|
+
remainingTables?: string[];
|
|
486
|
+
database?: DatabasePlan;
|
|
487
|
+
reusableBlocks?: ReusableBlockProposal[];
|
|
488
|
+
message?: string;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
type AIHttpEnvelope = {
|
|
492
|
+
responseID?: string;
|
|
493
|
+
response?: AIResponsePayload | null;
|
|
494
|
+
action?: string;
|
|
495
|
+
target?: string;
|
|
496
|
+
questions?: string[];
|
|
497
|
+
tables?: TableResponse[];
|
|
498
|
+
roles?: RoleResponse[];
|
|
499
|
+
pages?: PageResponse[];
|
|
500
|
+
items?: DataItem[];
|
|
501
|
+
database?: DatabasePlan;
|
|
502
|
+
reusableBlocks?: ReusableBlockProposal[];
|
|
503
|
+
message?: string;
|
|
504
|
+
result?: {
|
|
505
|
+
responseID?: string;
|
|
506
|
+
response?: AIResponsePayload | null;
|
|
507
|
+
action?: string;
|
|
508
|
+
target?: string;
|
|
509
|
+
questions?: string[];
|
|
510
|
+
tables?: TableResponse[];
|
|
511
|
+
roles?: RoleResponse[];
|
|
512
|
+
pages?: PageResponse[];
|
|
513
|
+
items?: DataItem[];
|
|
514
|
+
database?: DatabasePlan;
|
|
515
|
+
reusableBlocks?: ReusableBlockProposal[];
|
|
516
|
+
message?: string;
|
|
517
|
+
};
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
type Message = {
|
|
521
|
+
sender: "User" | "AI";
|
|
522
|
+
text: string;
|
|
523
|
+
action?: MessageAction;
|
|
524
|
+
response?: unknown;
|
|
525
|
+
responseID?: string;
|
|
526
|
+
requestText?: string;
|
|
527
|
+
reusableBlocks?: ReusableBlockProposal[];
|
|
528
|
+
applied?: boolean;
|
|
529
|
+
/** A pending names card that a newer proposal replaced. */
|
|
530
|
+
superseded?: boolean;
|
|
531
|
+
/** A names card the user already continued to the schema step. */
|
|
532
|
+
continued?: boolean;
|
|
533
|
+
translationResults?: TranslationResult[];
|
|
534
|
+
/** Data-agent tables the AI still has to generate for this message, auto-fetched next. */
|
|
535
|
+
remainingTables?: string[];
|
|
536
|
+
loadingMore?: boolean;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
type PersistedChatState = {
|
|
540
|
+
messages: Message[];
|
|
541
|
+
responseID: string;
|
|
542
|
+
currentEndpoint?: string;
|
|
543
|
+
currentMessage: string;
|
|
544
|
+
chatDatabaseContext: Database | null;
|
|
545
|
+
selectedTable: string | null;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const MAX_PERSISTED_CHAT_STATE_LENGTH = 180_000;
|
|
549
|
+
const RECENT_MESSAGES_TO_KEEP = 30;
|
|
550
|
+
const RECENT_MESSAGES_FALLBACK = 8;
|
|
551
|
+
const PROTECTED_TABLES = new Set([
|
|
552
|
+
"users",
|
|
553
|
+
"sessions",
|
|
554
|
+
"assets",
|
|
555
|
+
"translations",
|
|
556
|
+
"pages",
|
|
557
|
+
"blocks",
|
|
558
|
+
"dashboards",
|
|
559
|
+
"templates",
|
|
560
|
+
"passkey_credentials",
|
|
561
|
+
"passkey_challenges",
|
|
562
|
+
]);
|
|
563
|
+
const DEFAULT_TRANSLATION_KEYS = new Set([
|
|
564
|
+
"name",
|
|
565
|
+
"slug",
|
|
566
|
+
"description",
|
|
567
|
+
"title",
|
|
568
|
+
"content",
|
|
569
|
+
"type",
|
|
570
|
+
"status",
|
|
571
|
+
"date",
|
|
572
|
+
"time",
|
|
573
|
+
"size",
|
|
574
|
+
"link",
|
|
575
|
+
"label",
|
|
576
|
+
"icon",
|
|
577
|
+
"username",
|
|
578
|
+
"email",
|
|
579
|
+
"password",
|
|
580
|
+
"role",
|
|
581
|
+
"item",
|
|
582
|
+
]);
|
|
583
|
+
|
|
584
|
+
const collectTranslationKeys = (
|
|
585
|
+
field: Record<string, unknown>,
|
|
586
|
+
parentPath = "",
|
|
587
|
+
seen = new Set<string>(),
|
|
588
|
+
): string[] => {
|
|
589
|
+
const key = typeof field.key === "string" ? field.key.trim() : "";
|
|
590
|
+
if (!key) return [];
|
|
591
|
+
if (DEFAULT_TRANSLATION_KEYS.has(key.toLowerCase())) return [];
|
|
592
|
+
|
|
593
|
+
const path = parentPath ? `${parentPath}.${key}` : key;
|
|
594
|
+
const children = Array.isArray(field.children)
|
|
595
|
+
? field.children
|
|
596
|
+
: isRecord(field.children)
|
|
597
|
+
? [field.children]
|
|
598
|
+
: [];
|
|
599
|
+
|
|
600
|
+
if (!children.length) {
|
|
601
|
+
return [path];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const keys: string[] = [];
|
|
605
|
+
for (const child of children) {
|
|
606
|
+
if (!isRecord(child)) continue;
|
|
607
|
+
for (const nestedKey of collectTranslationKeys(child, path, seen)) {
|
|
608
|
+
if (!seen.has(nestedKey)) {
|
|
609
|
+
seen.add(nestedKey);
|
|
610
|
+
keys.push(nestedKey);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return keys;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const summarizeTranslationContext = (
|
|
619
|
+
tables: Array<{ slug: string; schema?: Schema }> = [],
|
|
620
|
+
): Record<string, string[]> => {
|
|
621
|
+
const summarized: Record<string, string[]> = {};
|
|
622
|
+
for (const table of tables) {
|
|
623
|
+
if (typeof table.slug !== "string" || PROTECTED_TABLES.has(table.slug))
|
|
624
|
+
continue;
|
|
625
|
+
const keys = new Set<string>();
|
|
626
|
+
for (const field of Array.isArray(table.schema) ? table.schema : []) {
|
|
627
|
+
if (!isRecord(field)) continue;
|
|
628
|
+
for (const key of collectTranslationKeys(field, "", keys)) {
|
|
629
|
+
keys.add(key);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (keys.size) summarized[table.slug] = Array.from(keys).sort();
|
|
633
|
+
}
|
|
634
|
+
return summarized;
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const database = useState<Database>("database");
|
|
638
|
+
const Language = useScopedCookie<LanguagesType>(
|
|
639
|
+
"language",
|
|
640
|
+
database.value?.slug,
|
|
641
|
+
);
|
|
642
|
+
const isRTL = computed(() => Language.value === "ar");
|
|
643
|
+
const sessionID = useScopedCookie<string>("sid", database.value?.slug);
|
|
644
|
+
const platformSessionID = useScopedCookie<string>("sid", "inicontent");
|
|
645
|
+
|
|
646
|
+
const currentEndpoint = ref(props.endpoint);
|
|
647
|
+
const subscriptionChecked = ref(false);
|
|
648
|
+
|
|
649
|
+
const routeDatabaseSlug = computed(() => {
|
|
650
|
+
const value = route.params.database;
|
|
651
|
+
if (Array.isArray(value)) return value[0] ?? "";
|
|
652
|
+
return value ? String(value) : "";
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const pricingPath = computed(() => {
|
|
656
|
+
const slug =
|
|
657
|
+
routeDatabaseSlug.value ||
|
|
658
|
+
databaseModel.value?.slug ||
|
|
659
|
+
database.value?.slug ||
|
|
660
|
+
"";
|
|
661
|
+
if (slug && slug !== "inicontent") return `/${slug}/pricing`;
|
|
662
|
+
return "/pricing";
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const goToPricing = async () => {
|
|
666
|
+
await navigateTo(pricingPath.value);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
670
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
671
|
+
|
|
672
|
+
const isTableResponse = (value: unknown): value is TableResponse =>
|
|
673
|
+
isRecord(value) &&
|
|
674
|
+
typeof value.slug === "string" &&
|
|
675
|
+
Array.isArray(value.schema);
|
|
676
|
+
|
|
677
|
+
const isTableNameProposal = (value: unknown): value is TableNameProposal =>
|
|
678
|
+
isRecord(value) &&
|
|
679
|
+
typeof value.slug === "string" &&
|
|
680
|
+
value.slug.trim().length > 0;
|
|
681
|
+
|
|
682
|
+
const isRoleResponse = (value: unknown): value is RoleResponse => {
|
|
683
|
+
if (
|
|
684
|
+
!isRecord(value) ||
|
|
685
|
+
typeof value.role !== "string" ||
|
|
686
|
+
!Array.isArray(value.permissions)
|
|
687
|
+
) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return value.permissions.every(
|
|
692
|
+
(permission) =>
|
|
693
|
+
isRecord(permission) &&
|
|
694
|
+
typeof permission.table === "string" &&
|
|
695
|
+
typeof permission.allowedMethods === "string",
|
|
696
|
+
);
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const isPageResponse = (value: unknown): value is PageResponse =>
|
|
700
|
+
isRecord(value) && typeof value.slug === "string";
|
|
701
|
+
|
|
702
|
+
const isReusableBlockProposal = (
|
|
703
|
+
value: unknown,
|
|
704
|
+
): value is ReusableBlockProposal =>
|
|
705
|
+
isRecord(value) && typeof value.name === "string";
|
|
706
|
+
|
|
707
|
+
const isDataItem = (value: unknown): value is DataItem =>
|
|
708
|
+
isRecord(value) &&
|
|
709
|
+
typeof value.table === "string" &&
|
|
710
|
+
Array.isArray(value.records);
|
|
711
|
+
|
|
712
|
+
const isTranslationPlanItem = (value: unknown): value is TranslationPlanItem =>
|
|
713
|
+
isRecord(value) &&
|
|
714
|
+
typeof value.table === "string" &&
|
|
715
|
+
typeof value.locale === "string" &&
|
|
716
|
+
!Array.isArray(value.records);
|
|
717
|
+
|
|
718
|
+
const isDatabasePlan = (value: unknown): value is DatabasePlan =>
|
|
719
|
+
isRecord(value) &&
|
|
720
|
+
typeof value.slug === "string" &&
|
|
721
|
+
value.slug.trim().length > 0;
|
|
722
|
+
|
|
723
|
+
const normalizeHexColor = (value?: string): string | undefined => {
|
|
724
|
+
if (!value) return;
|
|
725
|
+
const trimmed = value.trim();
|
|
726
|
+
if (!trimmed) return;
|
|
727
|
+
const withHash = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
728
|
+
return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(withHash)
|
|
729
|
+
? withHash.toLowerCase()
|
|
730
|
+
: undefined;
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const isAIResponsePayload = (value: unknown): value is AIResponsePayload =>
|
|
734
|
+
isRecord(value) && typeof value.action === "string";
|
|
735
|
+
|
|
736
|
+
const isMessage = (value: unknown): value is Message =>
|
|
737
|
+
isRecord(value) &&
|
|
738
|
+
(value.sender === "User" || value.sender === "AI") &&
|
|
739
|
+
typeof value.text === "string" &&
|
|
740
|
+
(value.action === undefined || typeof value.action === "string");
|
|
741
|
+
|
|
742
|
+
const migrateTranslationSummary = (message: Message): Message => {
|
|
743
|
+
if (message.sender !== "AI" || message.translationResults?.length)
|
|
744
|
+
return message;
|
|
745
|
+
|
|
746
|
+
const results = Array.from(
|
|
747
|
+
message.text.matchAll(/([\w-]+)→([\w-]+):\s*(\d+)\s+translations/g),
|
|
748
|
+
).map((match) => ({
|
|
749
|
+
table: match[1] as string,
|
|
750
|
+
locale: match[2] as string,
|
|
751
|
+
count: Number(match[3]),
|
|
752
|
+
success: true,
|
|
753
|
+
}));
|
|
754
|
+
|
|
755
|
+
return results.length
|
|
756
|
+
? { ...message, text: t("translated"), translationResults: results }
|
|
757
|
+
: message;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const createPersistedChatState = (
|
|
761
|
+
chatMessages: Message[],
|
|
762
|
+
): PersistedChatState => ({
|
|
763
|
+
messages: chatMessages,
|
|
764
|
+
responseID: responseID.value,
|
|
765
|
+
currentEndpoint: currentEndpoint.value,
|
|
766
|
+
currentMessage: currentMessage.value,
|
|
767
|
+
chatDatabaseContext: chatDatabaseContext.value,
|
|
768
|
+
selectedTable: selectedTable.value,
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
const serializePersistedChatState = (): string => {
|
|
772
|
+
const state = createPersistedChatState(messages.value);
|
|
773
|
+
let serialized = JSON.stringify(state);
|
|
774
|
+
if (serialized.length <= MAX_PERSISTED_CHAT_STATE_LENGTH) return serialized;
|
|
775
|
+
|
|
776
|
+
const importantMessages = new Set(
|
|
777
|
+
messages.value
|
|
778
|
+
.map((message, index) =>
|
|
779
|
+
message.action && message.action !== "error" ? index : -1,
|
|
780
|
+
)
|
|
781
|
+
.filter((index) => index >= 0),
|
|
782
|
+
);
|
|
783
|
+
const recentStart = Math.max(
|
|
784
|
+
0,
|
|
785
|
+
messages.value.length - RECENT_MESSAGES_TO_KEEP,
|
|
786
|
+
);
|
|
787
|
+
let trimmedMessages = messages.value.filter(
|
|
788
|
+
(_message, index) => importantMessages.has(index) || index >= recentStart,
|
|
789
|
+
);
|
|
790
|
+
serialized = JSON.stringify(createPersistedChatState(trimmedMessages));
|
|
791
|
+
if (serialized.length <= MAX_PERSISTED_CHAT_STATE_LENGTH) return serialized;
|
|
792
|
+
|
|
793
|
+
const fallbackStart = Math.max(
|
|
794
|
+
0,
|
|
795
|
+
messages.value.length - RECENT_MESSAGES_FALLBACK,
|
|
796
|
+
);
|
|
797
|
+
trimmedMessages = messages.value.filter(
|
|
798
|
+
(_message, index) => importantMessages.has(index) || index >= fallbackStart,
|
|
799
|
+
);
|
|
800
|
+
serialized = JSON.stringify(createPersistedChatState(trimmedMessages));
|
|
801
|
+
if (serialized.length <= MAX_PERSISTED_CHAT_STATE_LENGTH) return serialized;
|
|
802
|
+
|
|
803
|
+
return JSON.stringify(
|
|
804
|
+
createPersistedChatState(
|
|
805
|
+
messages.value.slice(fallbackStart).map((message) => ({
|
|
806
|
+
...message,
|
|
807
|
+
response: message.action ? message.response : undefined,
|
|
808
|
+
text:
|
|
809
|
+
message.text.length > 4_000
|
|
810
|
+
? `${message.text.slice(0, 4_000)}...`
|
|
811
|
+
: message.text,
|
|
812
|
+
})),
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
818
|
+
|
|
819
|
+
const persistChatState = () => {
|
|
820
|
+
if (!import.meta.client || !props.persistKey) return;
|
|
821
|
+
try {
|
|
822
|
+
localStorage.setItem(props.persistKey, serializePersistedChatState());
|
|
823
|
+
} catch {
|
|
824
|
+
// localStorage unavailable or full — keep the active in-memory chat.
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
const schedulePersistChatState = () => {
|
|
829
|
+
if (!import.meta.client || !props.persistKey) return;
|
|
830
|
+
if (persistTimer) clearTimeout(persistTimer);
|
|
831
|
+
persistTimer = setTimeout(() => {
|
|
832
|
+
persistTimer = null;
|
|
833
|
+
persistChatState();
|
|
834
|
+
}, 250);
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const loadPersistedChatState = (): boolean => {
|
|
838
|
+
if (!import.meta.client || !props.persistKey) return false;
|
|
839
|
+
try {
|
|
840
|
+
const raw = localStorage.getItem(props.persistKey);
|
|
841
|
+
if (!raw) return false;
|
|
842
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
843
|
+
if (!isRecord(parsed)) return false;
|
|
844
|
+
|
|
845
|
+
if (Array.isArray(parsed.messages)) {
|
|
846
|
+
messages.value = parsed.messages
|
|
847
|
+
.filter(isMessage)
|
|
848
|
+
.map(migrateTranslationSummary);
|
|
849
|
+
}
|
|
850
|
+
if (typeof parsed.responseID === "string")
|
|
851
|
+
responseID.value = parsed.responseID;
|
|
852
|
+
if (typeof parsed.currentEndpoint === "string")
|
|
853
|
+
currentEndpoint.value = parsed.currentEndpoint;
|
|
854
|
+
if (typeof parsed.currentMessage === "string")
|
|
855
|
+
currentMessage.value = parsed.currentMessage;
|
|
856
|
+
if (typeof parsed.selectedTable === "string")
|
|
857
|
+
selectedTable.value = parsed.selectedTable;
|
|
858
|
+
else selectedTable.value = null;
|
|
859
|
+
if (isRecord(parsed.chatDatabaseContext)) {
|
|
860
|
+
chatDatabaseContext.value = parsed.chatDatabaseContext as Database;
|
|
861
|
+
} else {
|
|
862
|
+
chatDatabaseContext.value = null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return true;
|
|
866
|
+
} catch {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const getMessageTables = (message: Message): TableResponse[] =>
|
|
872
|
+
Array.isArray(message.response)
|
|
873
|
+
? message.response.filter(isTableResponse)
|
|
874
|
+
: [];
|
|
875
|
+
|
|
876
|
+
const getMessageNameTables = (message: Message): TableNameProposal[] =>
|
|
877
|
+
Array.isArray(message.response)
|
|
878
|
+
? message.response.filter(isTableNameProposal)
|
|
879
|
+
: [];
|
|
880
|
+
|
|
881
|
+
const getMessageRoles = (message: Message): RoleResponse[] =>
|
|
882
|
+
Array.isArray(message.response)
|
|
883
|
+
? message.response.filter(isRoleResponse)
|
|
884
|
+
: [];
|
|
885
|
+
|
|
886
|
+
const getMessagePages = (message: Message): PageResponse[] =>
|
|
887
|
+
Array.isArray(message.response)
|
|
888
|
+
? message.response.filter(isPageResponse)
|
|
889
|
+
: [];
|
|
890
|
+
|
|
891
|
+
const getMessageReusableBlocks = (message: Message): ReusableBlockProposal[] =>
|
|
892
|
+
Array.isArray(message.reusableBlocks) ? message.reusableBlocks : [];
|
|
893
|
+
|
|
894
|
+
const getMessageDataItems = (message: Message): DataItem[] =>
|
|
895
|
+
Array.isArray(message.response) ? message.response.filter(isDataItem) : [];
|
|
896
|
+
|
|
897
|
+
const getMessageTranslationPlan = (message: Message): TranslationPlanItem[] =>
|
|
898
|
+
Array.isArray(message.response)
|
|
899
|
+
? message.response.filter(isTranslationPlanItem)
|
|
900
|
+
: [];
|
|
901
|
+
|
|
902
|
+
const getMessageDatabasePlan = (message: Message): DatabasePlan | null =>
|
|
903
|
+
isDatabasePlan(message.response) ? message.response : null;
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Tables endpoint step selection: propose names first for fresh build flows
|
|
907
|
+
* and when refining a pending name-proposal card; fall back to the full
|
|
908
|
+
* schema step once the conversation reached schema approval (roles, deletes
|
|
909
|
+
* and schema edits keep working as before).
|
|
910
|
+
*/
|
|
911
|
+
const shouldUseNamesStep = (): boolean => {
|
|
912
|
+
if (currentEndpoint.value !== "tables") return false;
|
|
913
|
+
|
|
914
|
+
// The most recent actionable card decides the step.
|
|
915
|
+
let sawAdvancedNames = false;
|
|
916
|
+
for (let i = messages.value.length - 1; i >= 0; i--) {
|
|
917
|
+
const message = messages.value[i];
|
|
918
|
+
if (message.sender !== "AI" || !message.action) continue;
|
|
919
|
+
|
|
920
|
+
if (message.action === "tables_approval_pending") return false;
|
|
921
|
+
|
|
922
|
+
if (message.action === "tables_naming_pending") {
|
|
923
|
+
// A still-open names card → keep proposing names.
|
|
924
|
+
if (!message.continued && !message.superseded) return true;
|
|
925
|
+
// A continued/superseded names card means the schema flow took
|
|
926
|
+
// over — keep scanning for the real conversation state.
|
|
927
|
+
sawAdvancedNames = true;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Any other action card (roles, deletes, database plan...) — fall
|
|
932
|
+
// through to the fresh-flow rule below.
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Names already continued to the schema step → stay on the schema step
|
|
937
|
+
// (schema-stage clarifications must not loop back to name proposals).
|
|
938
|
+
if (sawAdvancedNames) return false;
|
|
939
|
+
|
|
940
|
+
// Fresh build flow (schema approval never reached) → start with names.
|
|
941
|
+
return !messages.value.some(
|
|
942
|
+
(message) => message.action === "tables_approval_pending",
|
|
943
|
+
);
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const isNestedDatabaseContext = computed(() => {
|
|
947
|
+
const slug = databaseModel.value?.slug || database.value?.slug || "";
|
|
948
|
+
return !!slug && slug !== "inicontent";
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const getWelcomeMessage = () => {
|
|
952
|
+
if (isNestedDatabaseContext.value) {
|
|
953
|
+
return t("chatWelcomeScopedDatabase");
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (currentEndpoint.value === "tables") {
|
|
957
|
+
return t("chatWelcomeTables");
|
|
958
|
+
} else if (currentEndpoint.value === "pages") {
|
|
959
|
+
return t("chatWelcomePages");
|
|
960
|
+
}
|
|
961
|
+
return t("chatWelcomeDefault");
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const getTableIcon = (tableSlug: string): string | undefined => {
|
|
965
|
+
for (let i = messages.value.length - 1; i >= 0; i--) {
|
|
966
|
+
const message = messages.value[i];
|
|
967
|
+
if (
|
|
968
|
+
message &&
|
|
969
|
+
message.action === "tables_approval_pending" &&
|
|
970
|
+
message.response
|
|
971
|
+
) {
|
|
972
|
+
const table = getMessageTables(message).find((t) => t.slug === tableSlug);
|
|
973
|
+
if (table?.icon) {
|
|
974
|
+
return table.icon.trim();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return undefined;
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const messages = ref<Message[]>([]);
|
|
982
|
+
|
|
983
|
+
const selectedTable = ref<string | null>(null);
|
|
984
|
+
const showTableModal = ref(false);
|
|
985
|
+
const previewModalTitleKey = ref<"tableDemo" | "dataPreview">("tableDemo");
|
|
986
|
+
const selectedTableForModal = ref<TableResponse | null>(null);
|
|
987
|
+
const dataTable = ref<{
|
|
988
|
+
title: string;
|
|
989
|
+
columns: DataTableColumns<Record<string, unknown>>;
|
|
990
|
+
data: Array<Record<string, unknown>>;
|
|
991
|
+
}>({
|
|
992
|
+
title: "",
|
|
993
|
+
columns: [],
|
|
994
|
+
data: [],
|
|
995
|
+
});
|
|
996
|
+
const createDemoTableColumns = (
|
|
997
|
+
schema: Schema,
|
|
998
|
+
): DataTableColumns<Record<string, unknown>> => {
|
|
999
|
+
return schema.map((field) => ({
|
|
1000
|
+
title: () =>
|
|
1001
|
+
h(NFlex, { wrap: false, align: "center", justify: "start" }, () => [
|
|
1002
|
+
getField(field).icon(),
|
|
1003
|
+
t(field.key),
|
|
1004
|
+
]),
|
|
1005
|
+
key: field.key,
|
|
1006
|
+
render(row: Record<string, unknown>) {
|
|
1007
|
+
return h(LazyColumn, { value: row[field.key], field: field });
|
|
1008
|
+
},
|
|
1009
|
+
}));
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
// Fallback for data preview when the target table's schema isn't in the
|
|
1013
|
+
// current context (columns are derived straight from the generated records).
|
|
1014
|
+
const createGenericColumns = (
|
|
1015
|
+
records: Array<Record<string, unknown>>,
|
|
1016
|
+
): DataTableColumns<Record<string, unknown>> => {
|
|
1017
|
+
const keys = new Set<string>();
|
|
1018
|
+
for (const record of records) {
|
|
1019
|
+
for (const key of Object.keys(record)) keys.add(key);
|
|
1020
|
+
}
|
|
1021
|
+
return Array.from(keys).map((key) => ({
|
|
1022
|
+
title: key,
|
|
1023
|
+
key,
|
|
1024
|
+
render(row: Record<string, unknown>) {
|
|
1025
|
+
const value = row[key];
|
|
1026
|
+
if (value === null || value === undefined) return "";
|
|
1027
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
1028
|
+
},
|
|
1029
|
+
}));
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
const tableModalHistory = ref<TableResponse[]>([]);
|
|
1033
|
+
|
|
1034
|
+
watch(showTableModal, (val) => {
|
|
1035
|
+
if (!val) tableModalHistory.value = [];
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
const openTableDemoModal = (table: TableResponse) => {
|
|
1039
|
+
if (showTableModal.value && selectedTableForModal.value) {
|
|
1040
|
+
tableModalHistory.value.push(selectedTableForModal.value);
|
|
1041
|
+
}
|
|
1042
|
+
previewModalTitleKey.value = "tableDemo";
|
|
1043
|
+
selectedTableForModal.value = table;
|
|
1044
|
+
dataTable.value.data = table.demo;
|
|
1045
|
+
dataTable.value.columns = createDemoTableColumns(table.schema);
|
|
1046
|
+
dataTable.value.title =
|
|
1047
|
+
table.slug.charAt(0).toUpperCase() + table.slug.slice(1);
|
|
1048
|
+
showTableModal.value = true;
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
const undoTableDemoModal = () => {
|
|
1052
|
+
const prev = tableModalHistory.value.pop();
|
|
1053
|
+
if (prev) openTableDemoModal(prev);
|
|
1054
|
+
else showTableModal.value = false;
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// Preview a data-agent item's generated records before publishing them.
|
|
1058
|
+
const openDataItemPreviewModal = (item: DataItem) => {
|
|
1059
|
+
tableModalHistory.value = [];
|
|
1060
|
+
selectedTableForModal.value = null;
|
|
1061
|
+
previewModalTitleKey.value = "dataPreview";
|
|
1062
|
+
const tableObj = activeDatabase.value?.tables?.find(
|
|
1063
|
+
(t): t is { slug: string; schema: Schema } =>
|
|
1064
|
+
t.slug === item.table && Array.isArray(t.schema),
|
|
1065
|
+
);
|
|
1066
|
+
dataTable.value = {
|
|
1067
|
+
title: item.table.charAt(0).toUpperCase() + item.table.slice(1),
|
|
1068
|
+
columns: tableObj
|
|
1069
|
+
? createDemoTableColumns(tableObj.schema)
|
|
1070
|
+
: createGenericColumns(item.records),
|
|
1071
|
+
data: item.records,
|
|
1072
|
+
};
|
|
1073
|
+
showTableModal.value = true;
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
const showPageModal = ref(false);
|
|
1077
|
+
const selectedPageData = ref<PageResponse | null>(null);
|
|
1078
|
+
|
|
1079
|
+
const openPagePreviewModal = (page: PageResponse) => {
|
|
1080
|
+
selectedPageData.value = page;
|
|
1081
|
+
showPageModal.value = true;
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
const user = useState<User>("user");
|
|
1085
|
+
const config = useRuntimeConfig();
|
|
1086
|
+
const chatDatabaseContext = ref<Database | null>(null);
|
|
1087
|
+
const isAdmin = computed(
|
|
1088
|
+
() =>
|
|
1089
|
+
!!user.value?.id &&
|
|
1090
|
+
(user.value?.role === config.public.idOne ||
|
|
1091
|
+
database.value?.slug === "inicontent"),
|
|
1092
|
+
);
|
|
1093
|
+
const applyingIndex = ref<number | null>(null);
|
|
1094
|
+
const applyingDataIndex = ref<number | null>(null);
|
|
1095
|
+
const applyingTranslateIndex = ref<number | null>(null);
|
|
1096
|
+
const applyingDatabaseIndex = ref<number | null>(null);
|
|
1097
|
+
const activeDatabaseSlug = computed(
|
|
1098
|
+
() =>
|
|
1099
|
+
chatDatabaseContext.value?.slug ||
|
|
1100
|
+
databaseModel.value?.slug ||
|
|
1101
|
+
database.value?.slug ||
|
|
1102
|
+
"",
|
|
1103
|
+
);
|
|
1104
|
+
const activeDatabase = computed(
|
|
1105
|
+
() => chatDatabaseContext.value ?? databaseModel.value ?? database.value,
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
const buildRequestParams = (targetSlug: string) => {
|
|
1109
|
+
const scopedSession = useScopedCookie<string>("sid", targetSlug);
|
|
1110
|
+
const params: Record<string, string> = {
|
|
1111
|
+
locale: Language.value,
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
if (scopedSession.value) params[`${targetSlug}_sid`] = scopedSession.value;
|
|
1115
|
+
else if (sessionID.value) params[`${targetSlug}_sid`] = sessionID.value;
|
|
1116
|
+
|
|
1117
|
+
if (platformSessionID.value) {
|
|
1118
|
+
params.inicontent_sid = platformSessionID.value;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return params;
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
const resolveAIHttpEnvelope = (payload: AIHttpEnvelope) => {
|
|
1125
|
+
if (payload?.result && isAIResponsePayload(payload.result.response)) {
|
|
1126
|
+
return {
|
|
1127
|
+
responseID: payload.result.responseID,
|
|
1128
|
+
response: payload.result.response,
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (payload?.result && isAIResponsePayload(payload.result)) {
|
|
1133
|
+
return {
|
|
1134
|
+
responseID: undefined,
|
|
1135
|
+
response: payload.result,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (isAIResponsePayload(payload?.response)) {
|
|
1140
|
+
return {
|
|
1141
|
+
responseID: payload?.responseID,
|
|
1142
|
+
response: payload.response,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (isAIResponsePayload(payload)) {
|
|
1147
|
+
return {
|
|
1148
|
+
responseID: undefined,
|
|
1149
|
+
response: payload,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
return {
|
|
1154
|
+
responseID: payload?.responseID,
|
|
1155
|
+
response: payload?.response,
|
|
1156
|
+
};
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
const applyDatabase = async (message: Message, index: number) => {
|
|
1160
|
+
if (applyingDatabaseIndex.value !== null) return;
|
|
1161
|
+
|
|
1162
|
+
const plan = getMessageDatabasePlan(message);
|
|
1163
|
+
if (!plan?.slug) {
|
|
1164
|
+
messages.value.push({
|
|
1165
|
+
sender: "AI",
|
|
1166
|
+
action: "error",
|
|
1167
|
+
text: "No valid database plan was returned by AI.",
|
|
1168
|
+
});
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
applyingDatabaseIndex.value = index;
|
|
1173
|
+
try {
|
|
1174
|
+
const normalizedPrimaryColor = normalizeHexColor(plan.primaryColor);
|
|
1175
|
+
|
|
1176
|
+
const created = await $fetch<apiResponse<Database>>(
|
|
1177
|
+
`${config.public.apiBase}inicontent/databases/${encodeURIComponent(plan.slug)}`,
|
|
1178
|
+
{
|
|
1179
|
+
method: "POST",
|
|
1180
|
+
body: {
|
|
1181
|
+
slug: plan.slug,
|
|
1182
|
+
primaryLanguage: plan.primaryLanguage,
|
|
1183
|
+
secondaryLanguages: plan.secondaryLanguages,
|
|
1184
|
+
primaryColor: normalizedPrimaryColor,
|
|
1185
|
+
},
|
|
1186
|
+
params: buildRequestParams("inicontent"),
|
|
1187
|
+
credentials: "include",
|
|
1188
|
+
},
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
if (!created?.result) {
|
|
1192
|
+
messages.value.push({
|
|
1193
|
+
sender: "AI",
|
|
1194
|
+
action: "error",
|
|
1195
|
+
text: created?.message || "Failed to create database.",
|
|
1196
|
+
});
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
const createdSlug = created.result.slug;
|
|
1201
|
+
if (!createdSlug) {
|
|
1202
|
+
messages.value.push({
|
|
1203
|
+
sender: "AI",
|
|
1204
|
+
action: "error",
|
|
1205
|
+
text: "Database was created but no valid slug was returned.",
|
|
1206
|
+
});
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
message.applied = true;
|
|
1211
|
+
chatDatabaseContext.value = created.result;
|
|
1212
|
+
if (databaseModel.value) databaseModel.value = created.result;
|
|
1213
|
+
|
|
1214
|
+
messages.value.push({
|
|
1215
|
+
sender: "AI",
|
|
1216
|
+
text: `Database ${createdSlug} created successfully. Generating tables now...`,
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
const tablePrompt =
|
|
1220
|
+
plan.tablesPrompt ||
|
|
1221
|
+
`Generate the required tables for the ${createdSlug} database.`;
|
|
1222
|
+
|
|
1223
|
+
const generated = await $fetch<AIHttpEnvelope>(
|
|
1224
|
+
`${config.public.apiBase}${createdSlug}/ai/tables`,
|
|
1225
|
+
{
|
|
1226
|
+
method: "POST",
|
|
1227
|
+
headers: { "Content-Type": "application/json" },
|
|
1228
|
+
body: JSON.stringify({
|
|
1229
|
+
message: tablePrompt,
|
|
1230
|
+
step: "names",
|
|
1231
|
+
existingTables: (created.result.tables || [])
|
|
1232
|
+
.filter(
|
|
1233
|
+
(table): table is { slug: string; schema: Schema } =>
|
|
1234
|
+
typeof table.slug === "string" && Array.isArray(table.schema),
|
|
1235
|
+
)
|
|
1236
|
+
.map((table) => ({
|
|
1237
|
+
slug: table.slug,
|
|
1238
|
+
schema: table.schema as Schema,
|
|
1239
|
+
})),
|
|
1240
|
+
}),
|
|
1241
|
+
params: buildRequestParams(createdSlug),
|
|
1242
|
+
credentials: "include",
|
|
1243
|
+
},
|
|
1244
|
+
);
|
|
1245
|
+
|
|
1246
|
+
const normalized = resolveAIHttpEnvelope(generated);
|
|
1247
|
+
const response = normalized.response;
|
|
1248
|
+
|
|
1249
|
+
if (!response || typeof response.action !== "string") {
|
|
1250
|
+
throw new Error("AI returned an invalid response for table generation.");
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (
|
|
1254
|
+
response.action === "clarification_needed" &&
|
|
1255
|
+
response.questions?.length
|
|
1256
|
+
) {
|
|
1257
|
+
messages.value.push({
|
|
1258
|
+
sender: "AI",
|
|
1259
|
+
text: response.questions.map((q, i) => `${i + 1}. ${q}`).join("\n"),
|
|
1260
|
+
});
|
|
1261
|
+
} else if (response.action === "tables_naming_pending") {
|
|
1262
|
+
const names = Array.isArray(response.tables)
|
|
1263
|
+
? response.tables.filter(isTableNameProposal)
|
|
1264
|
+
: [];
|
|
1265
|
+
|
|
1266
|
+
if (!names.length) {
|
|
1267
|
+
throw new Error("AI returned no valid table name proposals.");
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
messages.value.push({
|
|
1271
|
+
sender: "AI",
|
|
1272
|
+
action: "tables_naming_pending",
|
|
1273
|
+
response: names,
|
|
1274
|
+
responseID: normalized.responseID,
|
|
1275
|
+
requestText: tablePrompt,
|
|
1276
|
+
text:
|
|
1277
|
+
response.message ||
|
|
1278
|
+
"Review the proposed table names before continuing.",
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
// Follow-up chat turns continue from the names conversation.
|
|
1282
|
+
if (normalized.responseID) responseID.value = normalized.responseID;
|
|
1283
|
+
} else if (response.message) {
|
|
1284
|
+
messages.value.push({
|
|
1285
|
+
sender: "AI",
|
|
1286
|
+
action: response.action as MessageAction,
|
|
1287
|
+
text: response.message,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1292
|
+
} catch (error: unknown) {
|
|
1293
|
+
console.error("Error creating database from AI plan:", error);
|
|
1294
|
+
const apiError = error as {
|
|
1295
|
+
data?: { result?: { message?: string }; message?: string };
|
|
1296
|
+
message?: string;
|
|
1297
|
+
};
|
|
1298
|
+
const errorMsg =
|
|
1299
|
+
apiError?.data?.result?.message ||
|
|
1300
|
+
apiError?.data?.message ||
|
|
1301
|
+
apiError?.message ||
|
|
1302
|
+
t("error");
|
|
1303
|
+
messages.value.push({
|
|
1304
|
+
sender: "AI",
|
|
1305
|
+
action: "error",
|
|
1306
|
+
text: errorMsg,
|
|
1307
|
+
});
|
|
1308
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1309
|
+
} finally {
|
|
1310
|
+
applyingDatabaseIndex.value = null;
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
const applyTables = async (message: Message, index: number) => {
|
|
1315
|
+
if (applyingIndex.value !== null) return;
|
|
1316
|
+
const tables = getMessageTables(message);
|
|
1317
|
+
if (!tables.length) {
|
|
1318
|
+
messages.value.push({
|
|
1319
|
+
sender: "AI",
|
|
1320
|
+
action: "error",
|
|
1321
|
+
text: "No valid table definitions were returned by AI.",
|
|
1322
|
+
});
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (!activeDatabaseSlug.value) {
|
|
1327
|
+
messages.value.push({
|
|
1328
|
+
sender: "AI",
|
|
1329
|
+
action: "error",
|
|
1330
|
+
text: "The current database is not available.",
|
|
1331
|
+
});
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
applyingIndex.value = index;
|
|
1336
|
+
try {
|
|
1337
|
+
const data = await $fetch<{
|
|
1338
|
+
results?: {
|
|
1339
|
+
slug: string;
|
|
1340
|
+
action: string;
|
|
1341
|
+
success: boolean;
|
|
1342
|
+
error?: string;
|
|
1343
|
+
}[];
|
|
1344
|
+
tables?: TableResponse[];
|
|
1345
|
+
result?: {
|
|
1346
|
+
results?: {
|
|
1347
|
+
slug: string;
|
|
1348
|
+
action: string;
|
|
1349
|
+
success: boolean;
|
|
1350
|
+
error?: string;
|
|
1351
|
+
}[];
|
|
1352
|
+
tables?: TableResponse[];
|
|
1353
|
+
};
|
|
1354
|
+
}>(`${config.public.apiBase}${activeDatabaseSlug.value}/ai/tables`, {
|
|
1355
|
+
method: "PUT",
|
|
1356
|
+
headers: { "Content-Type": "application/json" },
|
|
1357
|
+
body: JSON.stringify({ tables }),
|
|
1358
|
+
params: buildRequestParams(activeDatabaseSlug.value),
|
|
1359
|
+
credentials: "include",
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
const normalized = data.result ?? data;
|
|
1363
|
+
|
|
1364
|
+
const failed = (normalized.results || []).filter((r) => !r.success);
|
|
1365
|
+
if (failed.length) {
|
|
1366
|
+
messages.value.push({
|
|
1367
|
+
sender: "AI",
|
|
1368
|
+
action: "error",
|
|
1369
|
+
text: `Failed to apply some tables: ${failed.map((f) => `${f.slug} (${f.error})`).join(", ")}`,
|
|
1370
|
+
});
|
|
1371
|
+
} else {
|
|
1372
|
+
message.applied = true;
|
|
1373
|
+
// Refresh database tables state
|
|
1374
|
+
if (normalized.tables) {
|
|
1375
|
+
if (database.value?.slug === activeDatabaseSlug.value)
|
|
1376
|
+
database.value.tables = normalized.tables;
|
|
1377
|
+
if (databaseModel.value?.slug === activeDatabaseSlug.value)
|
|
1378
|
+
databaseModel.value.tables = normalized.tables;
|
|
1379
|
+
if (chatDatabaseContext.value?.slug === activeDatabaseSlug.value)
|
|
1380
|
+
chatDatabaseContext.value.tables = normalized.tables;
|
|
1381
|
+
}
|
|
1382
|
+
messages.value.push({
|
|
1383
|
+
sender: "AI",
|
|
1384
|
+
text: `${(normalized.results || []).map((r) => `${r.slug} (${t(r.action)})`).join(", ")}`,
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1389
|
+
} catch (error: unknown) {
|
|
1390
|
+
console.error("Error applying tables:", error);
|
|
1391
|
+
const apiError = error as {
|
|
1392
|
+
data?: { result?: { message?: string }; message?: string };
|
|
1393
|
+
message?: string;
|
|
1394
|
+
};
|
|
1395
|
+
const errorMsg =
|
|
1396
|
+
apiError?.data?.result?.message ||
|
|
1397
|
+
apiError?.data?.message ||
|
|
1398
|
+
apiError?.message ||
|
|
1399
|
+
t("error");
|
|
1400
|
+
messages.value.push({
|
|
1401
|
+
sender: "AI",
|
|
1402
|
+
action: "error",
|
|
1403
|
+
text: errorMsg,
|
|
1404
|
+
});
|
|
1405
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1406
|
+
} finally {
|
|
1407
|
+
applyingIndex.value = null;
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
const continueToSchema = async (message: Message, index: number) => {
|
|
1412
|
+
if (applyingIndex.value !== null) return;
|
|
1413
|
+
const names = getMessageNameTables(message);
|
|
1414
|
+
if (!names.length) {
|
|
1415
|
+
messages.value.push({
|
|
1416
|
+
sender: "AI",
|
|
1417
|
+
action: "error",
|
|
1418
|
+
text: "No valid table name proposals were returned by AI.",
|
|
1419
|
+
});
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (!activeDatabaseSlug.value) {
|
|
1424
|
+
messages.value.push({
|
|
1425
|
+
sender: "AI",
|
|
1426
|
+
action: "error",
|
|
1427
|
+
text: "The current database is not available.",
|
|
1428
|
+
});
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
applyingIndex.value = index;
|
|
1433
|
+
try {
|
|
1434
|
+
const requestText = message.requestText?.trim();
|
|
1435
|
+
const messageText = requestText
|
|
1436
|
+
? `${requestText}\n\nDesign the full schemas for the approved table names.`
|
|
1437
|
+
: `Design the full schemas for these approved table names:\n${names
|
|
1438
|
+
.map(
|
|
1439
|
+
(name) => `- ${name.slug}${name.label ? ` (${name.label})` : ""}`,
|
|
1440
|
+
)
|
|
1441
|
+
.join("\n")}`;
|
|
1442
|
+
|
|
1443
|
+
const data = await $fetch<AIHttpEnvelope>(
|
|
1444
|
+
`${config.public.apiBase}${activeDatabaseSlug.value}/ai/tables`,
|
|
1445
|
+
{
|
|
1446
|
+
method: "POST",
|
|
1447
|
+
headers: { "Content-Type": "application/json" },
|
|
1448
|
+
body: JSON.stringify({
|
|
1449
|
+
message: messageText,
|
|
1450
|
+
step: "schema",
|
|
1451
|
+
approvedTables: names.map((name) => ({
|
|
1452
|
+
slug: name.slug,
|
|
1453
|
+
label: name.label,
|
|
1454
|
+
icon: name.icon,
|
|
1455
|
+
})),
|
|
1456
|
+
existingTables: (activeDatabase.value?.tables || [])
|
|
1457
|
+
.filter(
|
|
1458
|
+
(table): table is { slug: string; schema: Schema } =>
|
|
1459
|
+
typeof table.slug === "string" && Array.isArray(table.schema),
|
|
1460
|
+
)
|
|
1461
|
+
.map((table) => ({
|
|
1462
|
+
slug: table.slug,
|
|
1463
|
+
schema: table.schema as Schema,
|
|
1464
|
+
})),
|
|
1465
|
+
}),
|
|
1466
|
+
params: buildRequestParams(activeDatabaseSlug.value),
|
|
1467
|
+
credentials: "include",
|
|
1468
|
+
},
|
|
1469
|
+
);
|
|
1470
|
+
|
|
1471
|
+
const normalized = resolveAIHttpEnvelope(data);
|
|
1472
|
+
const response = normalized.response;
|
|
1473
|
+
if (!response || typeof response.action !== "string") {
|
|
1474
|
+
throw new Error("AI returned an invalid response for table generation.");
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// The schema conversation continues from this turn.
|
|
1478
|
+
if (normalized.responseID) responseID.value = normalized.responseID;
|
|
1479
|
+
message.continued = true;
|
|
1480
|
+
|
|
1481
|
+
if (
|
|
1482
|
+
response.action === "clarification_needed" &&
|
|
1483
|
+
response.questions?.length
|
|
1484
|
+
) {
|
|
1485
|
+
messages.value.push({
|
|
1486
|
+
sender: "AI",
|
|
1487
|
+
text: response.questions.map((q, i) => `${i + 1}. ${q}`).join("\n"),
|
|
1488
|
+
});
|
|
1489
|
+
} else if (response.action === "tables_approval_pending") {
|
|
1490
|
+
const tables = Array.isArray(response.tables)
|
|
1491
|
+
? response.tables.filter(isTableResponse)
|
|
1492
|
+
: [];
|
|
1493
|
+
|
|
1494
|
+
if (!tables.length) {
|
|
1495
|
+
throw new Error("AI returned no valid table definitions.");
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
messages.value.push({
|
|
1499
|
+
sender: "AI",
|
|
1500
|
+
action: "tables_approval_pending",
|
|
1501
|
+
response: tables,
|
|
1502
|
+
responseID: normalized.responseID,
|
|
1503
|
+
text:
|
|
1504
|
+
response.message ||
|
|
1505
|
+
"Review the generated tables before applying the changes.",
|
|
1506
|
+
});
|
|
1507
|
+
} else if (response.message) {
|
|
1508
|
+
messages.value.push({
|
|
1509
|
+
sender: "AI",
|
|
1510
|
+
action: response.action as MessageAction,
|
|
1511
|
+
text: response.message,
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1516
|
+
} catch (error: unknown) {
|
|
1517
|
+
console.error("Error continuing to schema generation:", error);
|
|
1518
|
+
const apiError = error as {
|
|
1519
|
+
data?: { result?: { message?: string }; message?: string };
|
|
1520
|
+
message?: string;
|
|
1521
|
+
};
|
|
1522
|
+
const errorMsg =
|
|
1523
|
+
apiError?.data?.result?.message ||
|
|
1524
|
+
apiError?.data?.message ||
|
|
1525
|
+
apiError?.message ||
|
|
1526
|
+
t("error");
|
|
1527
|
+
messages.value.push({
|
|
1528
|
+
sender: "AI",
|
|
1529
|
+
action: "error",
|
|
1530
|
+
text: errorMsg,
|
|
1531
|
+
});
|
|
1532
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1533
|
+
} finally {
|
|
1534
|
+
applyingIndex.value = null;
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
const applyData = async (message: Message, index: number) => {
|
|
1539
|
+
if (applyingDataIndex.value !== null) return;
|
|
1540
|
+
const items = getMessageDataItems(message);
|
|
1541
|
+
if (!items.length) {
|
|
1542
|
+
messages.value.push({
|
|
1543
|
+
sender: "AI",
|
|
1544
|
+
action: "error",
|
|
1545
|
+
text: "No valid data items were returned by AI.",
|
|
1546
|
+
});
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (!activeDatabaseSlug.value) {
|
|
1550
|
+
messages.value.push({
|
|
1551
|
+
sender: "AI",
|
|
1552
|
+
action: "error",
|
|
1553
|
+
text: "The current database is not available.",
|
|
1554
|
+
});
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
applyingDataIndex.value = index;
|
|
1559
|
+
try {
|
|
1560
|
+
const data = await $fetch<{
|
|
1561
|
+
results?: {
|
|
1562
|
+
table: string;
|
|
1563
|
+
count: number;
|
|
1564
|
+
success: boolean;
|
|
1565
|
+
error?: string;
|
|
1566
|
+
}[];
|
|
1567
|
+
result?: {
|
|
1568
|
+
results?: {
|
|
1569
|
+
table: string;
|
|
1570
|
+
count: number;
|
|
1571
|
+
success: boolean;
|
|
1572
|
+
error?: string;
|
|
1573
|
+
}[];
|
|
1574
|
+
};
|
|
1575
|
+
}>(`${config.public.apiBase}${activeDatabaseSlug.value}/ai/data`, {
|
|
1576
|
+
method: "PUT",
|
|
1577
|
+
headers: { "Content-Type": "application/json" },
|
|
1578
|
+
body: JSON.stringify({ items }),
|
|
1579
|
+
params: buildRequestParams(activeDatabaseSlug.value),
|
|
1580
|
+
credentials: "include",
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
const normalized = data.result ?? data;
|
|
1584
|
+
const failed = (normalized.results || []).filter((r) => !r.success);
|
|
1585
|
+
if (failed.length) {
|
|
1586
|
+
messages.value.push({
|
|
1587
|
+
sender: "AI",
|
|
1588
|
+
action: "error",
|
|
1589
|
+
text: `Failed to publish some data: ${failed.map((f) => `${f.table} (${f.error})`).join(", ")}`,
|
|
1590
|
+
});
|
|
1591
|
+
} else {
|
|
1592
|
+
message.applied = true;
|
|
1593
|
+
const summary = (normalized.results || [])
|
|
1594
|
+
.map((r) => `${r.table}: ${r.count} records`)
|
|
1595
|
+
.join(", ");
|
|
1596
|
+
messages.value.push({ sender: "AI", text: summary });
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1600
|
+
} catch (error: unknown) {
|
|
1601
|
+
console.error("Error publishing data:", error);
|
|
1602
|
+
const apiError = error as {
|
|
1603
|
+
data?: { result?: { message?: string }; message?: string };
|
|
1604
|
+
message?: string;
|
|
1605
|
+
};
|
|
1606
|
+
const errorMsg =
|
|
1607
|
+
apiError?.data?.result?.message ||
|
|
1608
|
+
apiError?.data?.message ||
|
|
1609
|
+
apiError?.message ||
|
|
1610
|
+
t("error");
|
|
1611
|
+
messages.value.push({ sender: "AI", action: "error", text: errorMsg });
|
|
1612
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1613
|
+
} finally {
|
|
1614
|
+
applyingDataIndex.value = null;
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
const applyTranslation = async (message: Message, index: number) => {
|
|
1619
|
+
if (applyingTranslateIndex.value !== null) return;
|
|
1620
|
+
const items = getMessageTranslationPlan(message);
|
|
1621
|
+
if (!items.length) {
|
|
1622
|
+
messages.value.push({
|
|
1623
|
+
sender: "AI",
|
|
1624
|
+
action: "error",
|
|
1625
|
+
text: "No valid translation plan returned by AI.",
|
|
1626
|
+
});
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
if (!activeDatabaseSlug.value) {
|
|
1630
|
+
messages.value.push({
|
|
1631
|
+
sender: "AI",
|
|
1632
|
+
action: "error",
|
|
1633
|
+
text: "The current database is not available.",
|
|
1634
|
+
});
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
applyingTranslateIndex.value = index;
|
|
1639
|
+
try {
|
|
1640
|
+
const data = await $fetch<{
|
|
1641
|
+
results?: {
|
|
1642
|
+
table: string;
|
|
1643
|
+
locale: string;
|
|
1644
|
+
count: number;
|
|
1645
|
+
success: boolean;
|
|
1646
|
+
error?: string;
|
|
1647
|
+
}[];
|
|
1648
|
+
result?: {
|
|
1649
|
+
results?: {
|
|
1650
|
+
table: string;
|
|
1651
|
+
locale: string;
|
|
1652
|
+
count: number;
|
|
1653
|
+
success: boolean;
|
|
1654
|
+
error?: string;
|
|
1655
|
+
}[];
|
|
1656
|
+
};
|
|
1657
|
+
}>(`${config.public.apiBase}${activeDatabaseSlug.value}/ai/translate`, {
|
|
1658
|
+
method: "PUT",
|
|
1659
|
+
headers: { "Content-Type": "application/json" },
|
|
1660
|
+
body: JSON.stringify({ items }),
|
|
1661
|
+
params: buildRequestParams(activeDatabaseSlug.value),
|
|
1662
|
+
credentials: "include",
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
const normalized = data.result ?? data;
|
|
1666
|
+
const failed = (normalized.results || []).filter((r) => !r.success);
|
|
1667
|
+
if (failed.length) {
|
|
1668
|
+
messages.value.push({
|
|
1669
|
+
sender: "AI",
|
|
1670
|
+
action: "error",
|
|
1671
|
+
text: `Failed to translate some tables: ${failed.map((f) => `${f.table}→${f.locale} (${f.error})`).join(", ")}`,
|
|
1672
|
+
});
|
|
1673
|
+
} else {
|
|
1674
|
+
message.applied = true;
|
|
1675
|
+
const results = (normalized.results || []).filter(
|
|
1676
|
+
(r): r is TranslationResult =>
|
|
1677
|
+
typeof r.table === "string" &&
|
|
1678
|
+
typeof r.locale === "string" &&
|
|
1679
|
+
typeof r.count === "number" &&
|
|
1680
|
+
typeof r.success === "boolean",
|
|
1681
|
+
);
|
|
1682
|
+
messages.value.push({
|
|
1683
|
+
sender: "AI",
|
|
1684
|
+
text: t("translated"),
|
|
1685
|
+
translationResults: results,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1690
|
+
} catch (error: unknown) {
|
|
1691
|
+
console.error("Error applying translations:", error);
|
|
1692
|
+
const apiError = error as {
|
|
1693
|
+
data?: { result?: { message?: string }; message?: string };
|
|
1694
|
+
message?: string;
|
|
1695
|
+
};
|
|
1696
|
+
const errorMsg =
|
|
1697
|
+
apiError?.data?.result?.message ||
|
|
1698
|
+
apiError?.data?.message ||
|
|
1699
|
+
apiError?.message ||
|
|
1700
|
+
t("error");
|
|
1701
|
+
messages.value.push({ sender: "AI", action: "error", text: errorMsg });
|
|
1702
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1703
|
+
} finally {
|
|
1704
|
+
applyingTranslateIndex.value = null;
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
const uniqueTables = computed(() => {
|
|
1709
|
+
const rolesMessage = messages.value.find(
|
|
1710
|
+
(msg) => msg.action === "roles_defined",
|
|
1711
|
+
);
|
|
1712
|
+
if (!rolesMessage) return [];
|
|
1713
|
+
const tables = new Set<string>();
|
|
1714
|
+
getMessageRoles(rolesMessage).forEach((roleEntry) => {
|
|
1715
|
+
roleEntry.permissions.forEach((permission) => {
|
|
1716
|
+
tables.add(permission.table);
|
|
1717
|
+
});
|
|
1718
|
+
});
|
|
1719
|
+
return Array.from(tables);
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
const rolesForSelectedTable = computed(() => {
|
|
1723
|
+
if (!selectedTable.value) return [];
|
|
1724
|
+
const rolesMessage = messages.value.find(
|
|
1725
|
+
(msg) => msg.action === "roles_defined",
|
|
1726
|
+
);
|
|
1727
|
+
if (!rolesMessage) return [];
|
|
1728
|
+
|
|
1729
|
+
const rolesMap = new Map<string, { role: string; allowedMethods: string }>();
|
|
1730
|
+
|
|
1731
|
+
getMessageRoles(rolesMessage).forEach((roleEntry) => {
|
|
1732
|
+
const permissionForTable = roleEntry.permissions.find(
|
|
1733
|
+
(p) => p.table === selectedTable.value,
|
|
1734
|
+
);
|
|
1735
|
+
if (permissionForTable) {
|
|
1736
|
+
rolesMap.set(roleEntry.role, {
|
|
1737
|
+
role: roleEntry.role,
|
|
1738
|
+
allowedMethods: permissionForTable.allowedMethods,
|
|
1739
|
+
});
|
|
1740
|
+
} else {
|
|
1741
|
+
// Ensure role is added even if it has no specific permissions for the selected table, showing all as denied
|
|
1742
|
+
if (!rolesMap.has(roleEntry.role)) {
|
|
1743
|
+
rolesMap.set(roleEntry.role, {
|
|
1744
|
+
role: roleEntry.role,
|
|
1745
|
+
allowedMethods: "",
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
return Array.from(rolesMap.values());
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
const selectRoleTable = (tableName: string) => {
|
|
1754
|
+
selectedTable.value = tableName;
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
watch(
|
|
1758
|
+
messages,
|
|
1759
|
+
(newMessages) => {
|
|
1760
|
+
const rolesMsg = newMessages.find((msg) => msg.action === "roles_defined");
|
|
1761
|
+
if (rolesMsg) {
|
|
1762
|
+
const tables = new Set<string>();
|
|
1763
|
+
getMessageRoles(rolesMsg).forEach((roleEntry) => {
|
|
1764
|
+
roleEntry.permissions.forEach((permission) => {
|
|
1765
|
+
tables.add(permission.table);
|
|
1766
|
+
});
|
|
1767
|
+
});
|
|
1768
|
+
const availableTables = Array.from(tables);
|
|
1769
|
+
if (
|
|
1770
|
+
availableTables.length > 0 &&
|
|
1771
|
+
(!selectedTable.value || !availableTables.includes(selectedTable.value))
|
|
1772
|
+
)
|
|
1773
|
+
selectRoleTable(availableTables[0] as string);
|
|
1774
|
+
}
|
|
1775
|
+
},
|
|
1776
|
+
{ deep: true, immediate: true },
|
|
1777
|
+
);
|
|
1778
|
+
|
|
1779
|
+
const getPermissionIcon = (
|
|
1780
|
+
allowedMethods: string,
|
|
1781
|
+
method: "c" | "r" | "u" | "d",
|
|
1782
|
+
) => {
|
|
1783
|
+
if (allowedMethods?.includes(method))
|
|
1784
|
+
return { name: "tabler:check", color: "green" };
|
|
1785
|
+
|
|
1786
|
+
return { name: "tabler:x", color: "red" };
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
const currentMessage = ref("");
|
|
1790
|
+
const responseID = ref("");
|
|
1791
|
+
|
|
1792
|
+
const loading = ref(false);
|
|
1793
|
+
const requestVersion = ref(0);
|
|
1794
|
+
|
|
1795
|
+
const clearPersistedChatState = () => {
|
|
1796
|
+
if (!import.meta.client || !props.persistKey) return;
|
|
1797
|
+
try {
|
|
1798
|
+
localStorage.removeItem(props.persistKey);
|
|
1799
|
+
} catch {
|
|
1800
|
+
// localStorage unavailable - nothing to clear.
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
const addWelcomeMessage = async () => {
|
|
1805
|
+
if (!subscriptionChecked.value || !hasActiveSubscription.value) return;
|
|
1806
|
+
if (messages.value.length > 0) return;
|
|
1807
|
+
messages.value.push({ sender: "AI", text: getWelcomeMessage() });
|
|
1808
|
+
await nextTick();
|
|
1809
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1812
|
+
const resetChat = async () => {
|
|
1813
|
+
requestVersion.value++;
|
|
1814
|
+
if (persistTimer) {
|
|
1815
|
+
clearTimeout(persistTimer);
|
|
1816
|
+
persistTimer = null;
|
|
1817
|
+
}
|
|
1818
|
+
clearPersistedChatState();
|
|
1819
|
+
messages.value = [];
|
|
1820
|
+
responseID.value = "";
|
|
1821
|
+
currentEndpoint.value = props.endpoint;
|
|
1822
|
+
currentMessage.value = "";
|
|
1823
|
+
chatDatabaseContext.value = null;
|
|
1824
|
+
selectedTable.value = null;
|
|
1825
|
+
showTableModal.value = false;
|
|
1826
|
+
showPageModal.value = false;
|
|
1827
|
+
selectedTableForModal.value = null;
|
|
1828
|
+
tableModalHistory.value = [];
|
|
1829
|
+
selectedPageData.value = null;
|
|
1830
|
+
userHasScrolledUp.value = false;
|
|
1831
|
+
loading.value = false;
|
|
1832
|
+
applyingIndex.value = null;
|
|
1833
|
+
applyingDataIndex.value = null;
|
|
1834
|
+
applyingTranslateIndex.value = null;
|
|
1835
|
+
applyingDatabaseIndex.value = null;
|
|
1836
|
+
await addWelcomeMessage();
|
|
1837
|
+
persistChatState();
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
defineExpose({ resetChat });
|
|
1841
|
+
|
|
1842
|
+
const dynamicPlaceholder = computed(() => {
|
|
1843
|
+
if (isNestedDatabaseContext.value) {
|
|
1844
|
+
return t("chatPlaceholderScopedDatabase");
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
return messages.value.length > 2
|
|
1848
|
+
? t("chatPlaceholderShort")
|
|
1849
|
+
: t("chatPlaceholder");
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
const scrollbarRef = ref();
|
|
1853
|
+
const inputRef = ref();
|
|
1854
|
+
const userHasScrolledUp = ref(false);
|
|
1855
|
+
|
|
1856
|
+
const handleEnterKey = (event: KeyboardEvent) => {
|
|
1857
|
+
// Allow default behavior (insert newline) when Shift+Enter is pressed
|
|
1858
|
+
if (event.shiftKey) return;
|
|
1859
|
+
|
|
1860
|
+
// Prevent default behavior (insert newline) and send message when Enter is pressed
|
|
1861
|
+
event.preventDefault();
|
|
1862
|
+
sendMessage();
|
|
1863
|
+
nextTick(() => {
|
|
1864
|
+
inputRef.value?.focus();
|
|
1865
|
+
inputRef.value?.$el?.querySelector("textarea")?.focus();
|
|
1866
|
+
});
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
const scrollToBottom = async () => {
|
|
1870
|
+
await nextTick();
|
|
1871
|
+
const scrollbarInstance = scrollbarRef.value;
|
|
1872
|
+
if (scrollbarInstance)
|
|
1873
|
+
scrollbarInstance.scrollTo({
|
|
1874
|
+
top: scrollbarInstance.scrollHeight,
|
|
1875
|
+
behavior: "smooth",
|
|
1876
|
+
});
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
const handleScroll = (e: Event) => {
|
|
1880
|
+
const target = e.target as HTMLElement;
|
|
1881
|
+
if (!target) return;
|
|
1882
|
+
|
|
1883
|
+
const { scrollTop, scrollHeight, clientHeight } = target;
|
|
1884
|
+
const threshold = 20; // Pixels from bottom to consider "at bottom"
|
|
1885
|
+
|
|
1886
|
+
// Check if user is close to the bottom
|
|
1887
|
+
userHasScrolledUp.value = scrollHeight - scrollTop - clientHeight > threshold;
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1890
|
+
// Automatically resumes a truncated multi-table data generation until no
|
|
1891
|
+
// tables remain, appending the newly generated tables onto the same message.
|
|
1892
|
+
const continueDataGeneration = async (dataMessage: Message) => {
|
|
1893
|
+
const requestSnapshot = requestVersion.value;
|
|
1894
|
+
let guard = 20; // hard cap against a misbehaving assistant looping forever
|
|
1895
|
+
while (dataMessage.remainingTables?.length && guard-- > 0) {
|
|
1896
|
+
try {
|
|
1897
|
+
const payload: {
|
|
1898
|
+
message: string;
|
|
1899
|
+
responseID?: string;
|
|
1900
|
+
existingTables?: Array<Pick<TableResponse, "slug" | "schema">>;
|
|
1901
|
+
} = {
|
|
1902
|
+
message: `Continue generating the remaining tables: ${dataMessage.remainingTables.join(", ")}.`,
|
|
1903
|
+
responseID: responseID.value || undefined,
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1906
|
+
if (activeDatabase.value?.tables?.length) {
|
|
1907
|
+
payload.existingTables = activeDatabase.value.tables
|
|
1908
|
+
.filter(
|
|
1909
|
+
(table): table is { slug: string; schema: Schema } =>
|
|
1910
|
+
typeof table.slug === "string" &&
|
|
1911
|
+
Array.isArray(table.schema) &&
|
|
1912
|
+
!PROTECTED_TABLES.has(table.slug),
|
|
1913
|
+
)
|
|
1914
|
+
.map((table) => ({
|
|
1915
|
+
slug: table.slug,
|
|
1916
|
+
schema: table.schema as Schema,
|
|
1917
|
+
}));
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const data = await $fetch<AIHttpEnvelope>(
|
|
1921
|
+
`${config.public.apiBase}${activeDatabaseSlug.value}/ai/data`,
|
|
1922
|
+
{
|
|
1923
|
+
method: "POST",
|
|
1924
|
+
headers: { "Content-Type": "application/json" },
|
|
1925
|
+
body: JSON.stringify(payload),
|
|
1926
|
+
params: buildRequestParams(activeDatabaseSlug.value),
|
|
1927
|
+
credentials: "include",
|
|
1928
|
+
},
|
|
1929
|
+
);
|
|
1930
|
+
|
|
1931
|
+
const normalized = resolveAIHttpEnvelope(data);
|
|
1932
|
+
if (requestSnapshot !== requestVersion.value) return;
|
|
1933
|
+
responseID.value = normalized.responseID ?? responseID.value;
|
|
1934
|
+
|
|
1935
|
+
const response = normalized.response;
|
|
1936
|
+
if (!response || response.action !== "data_approval_pending") {
|
|
1937
|
+
if (response?.message)
|
|
1938
|
+
messages.value.push({ sender: "AI", text: response.message });
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
const newItems = Array.isArray(response.items)
|
|
1943
|
+
? response.items.filter(isDataItem)
|
|
1944
|
+
: [];
|
|
1945
|
+
const existingItems = Array.isArray(dataMessage.response)
|
|
1946
|
+
? (dataMessage.response as DataItem[])
|
|
1947
|
+
: [];
|
|
1948
|
+
dataMessage.response = [...existingItems, ...newItems];
|
|
1949
|
+
dataMessage.remainingTables = Array.isArray(response.remainingTables)
|
|
1950
|
+
? response.remainingTables.filter(
|
|
1951
|
+
(t): t is string => typeof t === "string" && t.trim().length > 0,
|
|
1952
|
+
)
|
|
1953
|
+
: [];
|
|
1954
|
+
|
|
1955
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1956
|
+
} catch (error: unknown) {
|
|
1957
|
+
if (requestSnapshot !== requestVersion.value) return;
|
|
1958
|
+
console.error("Error continuing data generation:", error);
|
|
1959
|
+
const apiError = error as {
|
|
1960
|
+
data?: { result?: { message?: string }; message?: string };
|
|
1961
|
+
message?: string;
|
|
1962
|
+
};
|
|
1963
|
+
messages.value.push({
|
|
1964
|
+
sender: "AI",
|
|
1965
|
+
action: "error",
|
|
1966
|
+
text:
|
|
1967
|
+
apiError?.data?.result?.message ||
|
|
1968
|
+
apiError?.data?.message ||
|
|
1969
|
+
apiError?.message ||
|
|
1970
|
+
t("error"),
|
|
1971
|
+
});
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (requestSnapshot !== requestVersion.value) return;
|
|
1976
|
+
dataMessage.loadingMore = false;
|
|
1977
|
+
};
|
|
1978
|
+
|
|
1979
|
+
const sendMessage = async () => {
|
|
1980
|
+
const originalUserText = currentMessage.value.trim();
|
|
1981
|
+
if (originalUserText === "") return;
|
|
1982
|
+
const requestSnapshot = requestVersion.value;
|
|
1983
|
+
|
|
1984
|
+
loading.value = true;
|
|
1985
|
+
const userMessageToPush = originalUserText; // Store before clearing
|
|
1986
|
+
currentMessage.value = "";
|
|
1987
|
+
messages.value.push({ sender: "User", text: userMessageToPush });
|
|
1988
|
+
|
|
1989
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
1990
|
+
|
|
1991
|
+
let responseIdForPayload = responseID.value;
|
|
1992
|
+
|
|
1993
|
+
let keepTrying = true;
|
|
1994
|
+
let maxRedirects = 3;
|
|
1995
|
+
|
|
1996
|
+
try {
|
|
1997
|
+
if (!activeDatabaseSlug.value) {
|
|
1998
|
+
throw new Error("The current database is not available.");
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
while (keepTrying && maxRedirects > 0) {
|
|
2002
|
+
if (requestSnapshot !== requestVersion.value) return;
|
|
2003
|
+
maxRedirects--;
|
|
2004
|
+
keepTrying = false;
|
|
2005
|
+
|
|
2006
|
+
const payload: {
|
|
2007
|
+
message: string;
|
|
2008
|
+
step?: "names" | "schema";
|
|
2009
|
+
responseID?: string;
|
|
2010
|
+
existingTables?:
|
|
2011
|
+
| Record<string, string[]>
|
|
2012
|
+
| Array<{ slug: string; keys?: string[]; schema?: Schema }>;
|
|
2013
|
+
secondaryLanguages?: string[];
|
|
2014
|
+
primaryLanguage?: string;
|
|
2015
|
+
} = {
|
|
2016
|
+
message: userMessageToPush,
|
|
2017
|
+
responseID: responseIdForPayload || undefined,
|
|
2018
|
+
};
|
|
2019
|
+
|
|
2020
|
+
// Step selection for the tables endpoint: propose names first on
|
|
2021
|
+
// fresh build flows, then design schemas once names are approved.
|
|
2022
|
+
if (currentEndpoint.value === "tables") {
|
|
2023
|
+
payload.step = shouldUseNamesStep() ? "names" : "schema";
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// Include existing tables context when targeting the tables, data or
|
|
2027
|
+
// pages endpoint (pages need them for table-backed blocks like
|
|
2028
|
+
// Loop/Form/Product and their column-id mappings).
|
|
2029
|
+
if (
|
|
2030
|
+
(currentEndpoint.value === "tables" ||
|
|
2031
|
+
currentEndpoint.value === "data" ||
|
|
2032
|
+
currentEndpoint.value === "pages") &&
|
|
2033
|
+
activeDatabase.value?.tables?.length
|
|
2034
|
+
) {
|
|
2035
|
+
const systemTables = ["sessions", "assets", "translations"];
|
|
2036
|
+
payload.existingTables = activeDatabase.value.tables
|
|
2037
|
+
.filter(
|
|
2038
|
+
(table): table is { slug: string; schema: Schema } =>
|
|
2039
|
+
typeof table.slug === "string" &&
|
|
2040
|
+
Array.isArray(table.schema) &&
|
|
2041
|
+
!systemTables.includes(table.slug),
|
|
2042
|
+
)
|
|
2043
|
+
.map((table) => ({
|
|
2044
|
+
slug: table.slug,
|
|
2045
|
+
schema: table.schema as Schema,
|
|
2046
|
+
}));
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// Include language context when targeting the translate endpoint
|
|
2050
|
+
if (currentEndpoint.value === "translate") {
|
|
2051
|
+
if (activeDatabase.value?.secondaryLanguages?.length)
|
|
2052
|
+
payload.secondaryLanguages = activeDatabase.value
|
|
2053
|
+
.secondaryLanguages as string[];
|
|
2054
|
+
if (activeDatabase.value?.primaryLanguage)
|
|
2055
|
+
payload.primaryLanguage = activeDatabase.value
|
|
2056
|
+
.primaryLanguage as string;
|
|
2057
|
+
if (activeDatabase.value?.tables?.length) {
|
|
2058
|
+
payload.existingTables = summarizeTranslationContext(
|
|
2059
|
+
activeDatabase.value.tables,
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
const data = await $fetch<AIHttpEnvelope>(
|
|
2065
|
+
`${config.public.apiBase}${activeDatabaseSlug.value}/ai${
|
|
2066
|
+
currentEndpoint.value ? `/${currentEndpoint.value}` : ""
|
|
2067
|
+
}`,
|
|
2068
|
+
{
|
|
2069
|
+
method: "POST",
|
|
2070
|
+
headers: { "Content-Type": "application/json" },
|
|
2071
|
+
body: JSON.stringify(payload),
|
|
2072
|
+
params: buildRequestParams(activeDatabaseSlug.value),
|
|
2073
|
+
credentials: "include",
|
|
2074
|
+
},
|
|
2075
|
+
);
|
|
2076
|
+
|
|
2077
|
+
const normalized = resolveAIHttpEnvelope(data);
|
|
2078
|
+
if (requestSnapshot !== requestVersion.value) return;
|
|
2079
|
+
responseID.value = normalized.responseID ?? "";
|
|
2080
|
+
responseIdForPayload = normalized.responseID ?? "";
|
|
2081
|
+
|
|
2082
|
+
const response = normalized.response;
|
|
2083
|
+
if (!response || typeof response.action !== "string") {
|
|
2084
|
+
throw new Error("AI returned an invalid response.");
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (response.action === "redirect" && response.target) {
|
|
2088
|
+
const switchedEndpoint = currentEndpoint.value !== response.target;
|
|
2089
|
+
currentEndpoint.value = response.target;
|
|
2090
|
+
if (switchedEndpoint) {
|
|
2091
|
+
// Start a fresh chain for the specialized assistant so its own system prompt is applied.
|
|
2092
|
+
responseID.value = "";
|
|
2093
|
+
responseIdForPayload = "";
|
|
2094
|
+
}
|
|
2095
|
+
keepTrying = true;
|
|
2096
|
+
} else {
|
|
2097
|
+
if (
|
|
2098
|
+
response.action === "clarification_needed" &&
|
|
2099
|
+
response.questions?.length
|
|
2100
|
+
) {
|
|
2101
|
+
messages.value.push({
|
|
2102
|
+
sender: "AI",
|
|
2103
|
+
text: response.questions.map((q, i) => `${i + 1}. ${q}`).join("\n"),
|
|
2104
|
+
});
|
|
2105
|
+
} else if (response.action === "database_approval_pending") {
|
|
2106
|
+
const databasePlan = isDatabasePlan(response.database)
|
|
2107
|
+
? response.database
|
|
2108
|
+
: null;
|
|
2109
|
+
|
|
2110
|
+
if (!databasePlan) {
|
|
2111
|
+
throw new Error("AI returned no valid database plan.");
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
messages.value.push({
|
|
2115
|
+
sender: "AI",
|
|
2116
|
+
action: "database_approval_pending",
|
|
2117
|
+
response: databasePlan,
|
|
2118
|
+
text:
|
|
2119
|
+
response.message ||
|
|
2120
|
+
`Review database ${databasePlan.slug} before creating it.`,
|
|
2121
|
+
});
|
|
2122
|
+
} else if (response.action === "tables_naming_pending") {
|
|
2123
|
+
const names = Array.isArray(response.tables)
|
|
2124
|
+
? response.tables.filter(isTableNameProposal)
|
|
2125
|
+
: [];
|
|
2126
|
+
|
|
2127
|
+
if (!names.length) {
|
|
2128
|
+
throw new Error("AI returned no valid table name proposals.");
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// A newer proposal supersedes any earlier pending names card.
|
|
2132
|
+
for (const existing of messages.value) {
|
|
2133
|
+
if (
|
|
2134
|
+
existing.action === "tables_naming_pending" &&
|
|
2135
|
+
!existing.applied
|
|
2136
|
+
) {
|
|
2137
|
+
existing.superseded = true;
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
messages.value.push({
|
|
2142
|
+
sender: "AI",
|
|
2143
|
+
action: "tables_naming_pending",
|
|
2144
|
+
response: names,
|
|
2145
|
+
responseID: responseIdForPayload,
|
|
2146
|
+
requestText: userMessageToPush,
|
|
2147
|
+
text:
|
|
2148
|
+
response.message ||
|
|
2149
|
+
"Review the proposed table names before continuing.",
|
|
2150
|
+
});
|
|
2151
|
+
} else if (response.action === "tables_approval_pending") {
|
|
2152
|
+
const tables = Array.isArray(response.tables)
|
|
2153
|
+
? response.tables.filter(isTableResponse)
|
|
2154
|
+
: [];
|
|
2155
|
+
|
|
2156
|
+
if (!tables.length) {
|
|
2157
|
+
throw new Error("AI returned no valid table definitions.");
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
messages.value.push({
|
|
2161
|
+
sender: "AI",
|
|
2162
|
+
action: "tables_approval_pending",
|
|
2163
|
+
response: tables,
|
|
2164
|
+
text:
|
|
2165
|
+
response.message ||
|
|
2166
|
+
"Review the generated tables before applying the changes.",
|
|
2167
|
+
});
|
|
2168
|
+
} else if (response.action === "roles_defined") {
|
|
2169
|
+
const roles = Array.isArray(response.roles)
|
|
2170
|
+
? response.roles.filter(isRoleResponse)
|
|
2171
|
+
: [];
|
|
2172
|
+
|
|
2173
|
+
if (!roles.length) {
|
|
2174
|
+
throw new Error("AI returned no valid role definitions.");
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
messages.value.push({
|
|
2178
|
+
sender: "AI",
|
|
2179
|
+
action: "roles_defined",
|
|
2180
|
+
response: roles,
|
|
2181
|
+
text: response.message || "Roles have been defined.",
|
|
2182
|
+
});
|
|
2183
|
+
} else if (response.action === "structure_generated") {
|
|
2184
|
+
const pages = Array.isArray(response.pages)
|
|
2185
|
+
? response.pages.filter(isPageResponse)
|
|
2186
|
+
: [];
|
|
2187
|
+
|
|
2188
|
+
messages.value.push({
|
|
2189
|
+
sender: "AI",
|
|
2190
|
+
action: "structure_generated",
|
|
2191
|
+
response: pages.length ? pages : undefined,
|
|
2192
|
+
reusableBlocks: Array.isArray(response.reusableBlocks)
|
|
2193
|
+
? response.reusableBlocks.filter(isReusableBlockProposal)
|
|
2194
|
+
: undefined,
|
|
2195
|
+
text: response.message || "Here is the generated page structure.",
|
|
2196
|
+
});
|
|
2197
|
+
} else if (response.action === "data_approval_pending") {
|
|
2198
|
+
const items = Array.isArray(response.items)
|
|
2199
|
+
? response.items.filter(isDataItem)
|
|
2200
|
+
: [];
|
|
2201
|
+
|
|
2202
|
+
if (!items.length) {
|
|
2203
|
+
throw new Error("AI returned no valid data items.");
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
const remainingTables = Array.isArray(response.remainingTables)
|
|
2207
|
+
? response.remainingTables.filter(
|
|
2208
|
+
(t): t is string =>
|
|
2209
|
+
typeof t === "string" && t.trim().length > 0,
|
|
2210
|
+
)
|
|
2211
|
+
: [];
|
|
2212
|
+
|
|
2213
|
+
const dataMessage: Message = {
|
|
2214
|
+
sender: "AI",
|
|
2215
|
+
action: "data_approval_pending",
|
|
2216
|
+
response: items,
|
|
2217
|
+
remainingTables,
|
|
2218
|
+
loadingMore: remainingTables.length > 0,
|
|
2219
|
+
text:
|
|
2220
|
+
response.message ||
|
|
2221
|
+
"Review the generated records before publishing.",
|
|
2222
|
+
};
|
|
2223
|
+
messages.value.push(dataMessage);
|
|
2224
|
+
|
|
2225
|
+
if (remainingTables.length) continueDataGeneration(dataMessage);
|
|
2226
|
+
} else if (response.action === "translation_approval_pending") {
|
|
2227
|
+
const items = Array.isArray(response.items)
|
|
2228
|
+
? response.items.filter(isTranslationPlanItem)
|
|
2229
|
+
: [];
|
|
2230
|
+
|
|
2231
|
+
if (!items.length) {
|
|
2232
|
+
throw new Error("AI returned no valid translation plan.");
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
messages.value.push({
|
|
2236
|
+
sender: "AI",
|
|
2237
|
+
action: "translation_approval_pending",
|
|
2238
|
+
response: items,
|
|
2239
|
+
text:
|
|
2240
|
+
response.message ||
|
|
2241
|
+
"Review the translation plan before proceeding.",
|
|
2242
|
+
});
|
|
2243
|
+
} else if (response.message) {
|
|
2244
|
+
messages.value.push({
|
|
2245
|
+
sender: "AI",
|
|
2246
|
+
action: response.action as MessageAction,
|
|
2247
|
+
text: response.message,
|
|
2248
|
+
});
|
|
2249
|
+
} else {
|
|
2250
|
+
throw new Error("No message in AI response");
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
if (maxRedirects === 0 && keepTrying) {
|
|
2256
|
+
messages.value.push({
|
|
2257
|
+
sender: "AI",
|
|
2258
|
+
action: "error",
|
|
2259
|
+
text: "Too many redirects occurred.",
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
2264
|
+
} catch (error: unknown) {
|
|
2265
|
+
if (requestSnapshot !== requestVersion.value) return;
|
|
2266
|
+
console.error("Error sending message:", error);
|
|
2267
|
+
const apiError = error as {
|
|
2268
|
+
data?: { result?: { message?: string }; message?: string };
|
|
2269
|
+
message?: string;
|
|
2270
|
+
};
|
|
2271
|
+
const errorMsg =
|
|
2272
|
+
apiError?.data?.result?.message ||
|
|
2273
|
+
apiError?.data?.message ||
|
|
2274
|
+
apiError?.message ||
|
|
2275
|
+
t("error");
|
|
2276
|
+
messages.value.push({
|
|
2277
|
+
sender: "AI",
|
|
2278
|
+
action: "error",
|
|
2279
|
+
text: errorMsg,
|
|
2280
|
+
});
|
|
2281
|
+
if (!userHasScrolledUp.value) await scrollToBottom();
|
|
2282
|
+
}
|
|
2283
|
+
if (requestSnapshot === requestVersion.value) loading.value = false;
|
|
2284
|
+
};
|
|
2285
|
+
|
|
2286
|
+
onMounted(async () => {
|
|
2287
|
+
await loadSubscriptionData();
|
|
2288
|
+
subscriptionChecked.value = true;
|
|
2289
|
+
|
|
2290
|
+
if (!hasActiveSubscription.value) {
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
loadPersistedChatState();
|
|
2295
|
+
await addWelcomeMessage();
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
watch(
|
|
2299
|
+
[
|
|
2300
|
+
messages,
|
|
2301
|
+
responseID,
|
|
2302
|
+
currentEndpoint,
|
|
2303
|
+
currentMessage,
|
|
2304
|
+
chatDatabaseContext,
|
|
2305
|
+
selectedTable,
|
|
2306
|
+
],
|
|
2307
|
+
schedulePersistChatState,
|
|
2308
|
+
{ deep: true, flush: "post" },
|
|
2309
|
+
);
|
|
2310
|
+
|
|
2311
|
+
watch(
|
|
2312
|
+
() => props.resetNonce,
|
|
2313
|
+
(newValue, oldValue) => {
|
|
2314
|
+
if (newValue === oldValue) return;
|
|
2315
|
+
resetChat();
|
|
2316
|
+
},
|
|
2317
|
+
);
|
|
2318
|
+
|
|
2319
|
+
onBeforeUnmount(() => {
|
|
2320
|
+
if (persistTimer) {
|
|
2321
|
+
clearTimeout(persistTimer);
|
|
2322
|
+
persistTimer = null;
|
|
2323
|
+
persistChatState();
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
2326
|
+
</script>
|