@ansiversa/components 0.0.127 → 0.0.129
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/index.ts +1 -0
- package/package.json +1 -1
- package/src/components/Ai/AvAiAssist.astro +315 -0
package/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ export { default as FlashNoteSummary } from './src/Summary/FlashNoteSummary.astr
|
|
|
38
38
|
export { default as ResumeBuilderSummary } from './src/Summary/ResumeBuilderSummary.astro';
|
|
39
39
|
export { default as PortfolioCreatorSummary } from './src/Summary/PortfolioCreatorSummary.astro';
|
|
40
40
|
export { default as AvImageUploader } from "./src/components/media/AvImageUploader.astro";
|
|
41
|
+
export { default as AvAiAssist } from "./src/components/Ai/AvAiAssist.astro";
|
|
41
42
|
export { default as ResumeBuilderShell } from './src/resume-templates/ResumeBuilderShell.astro';
|
|
42
43
|
export { default as ResumeTemplateClassic } from './src/resume-templates/ResumeTemplateClassic.astro';
|
|
43
44
|
export { default as ResumeTemplateModernTwoTone } from './src/resume-templates/ResumeTemplateModernTwoTone.astro';
|
package/package.json
CHANGED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
featureKey: string;
|
|
4
|
+
value: string;
|
|
5
|
+
valueSourceSelector?: string;
|
|
6
|
+
minChars?: number;
|
|
7
|
+
maxChars?: number;
|
|
8
|
+
label?: string;
|
|
9
|
+
onAppendEvent?: string;
|
|
10
|
+
onReplaceEvent?: string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
featureKey,
|
|
16
|
+
value,
|
|
17
|
+
valueSourceSelector,
|
|
18
|
+
minChars = 30,
|
|
19
|
+
maxChars = 1500,
|
|
20
|
+
label = "AI",
|
|
21
|
+
onAppendEvent = "av:ai-append",
|
|
22
|
+
onReplaceEvent = "av:ai-replace",
|
|
23
|
+
...rest
|
|
24
|
+
} = Astro.props as Props;
|
|
25
|
+
|
|
26
|
+
const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
class="av-ai-assist"
|
|
31
|
+
data-av-ai-assist={componentId}
|
|
32
|
+
data-value={value}
|
|
33
|
+
{...rest}
|
|
34
|
+
x-data={`avAiAssist(${JSON.stringify({
|
|
35
|
+
featureKey,
|
|
36
|
+
valueSourceSelector: valueSourceSelector ?? null,
|
|
37
|
+
minChars,
|
|
38
|
+
maxChars,
|
|
39
|
+
label,
|
|
40
|
+
onAppendEvent,
|
|
41
|
+
onReplaceEvent,
|
|
42
|
+
})})`}
|
|
43
|
+
x-init="init()"
|
|
44
|
+
>
|
|
45
|
+
<span class="av-ai-assist-trigger" :title="disabledReason || `Get ${label} suggestions`">
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
class="av-btn-ghost av-btn-sm av-ai-assist-btn"
|
|
49
|
+
@click.prevent="openModal()"
|
|
50
|
+
:disabled="!canFetch"
|
|
51
|
+
aria-label="Get AI suggestions"
|
|
52
|
+
>
|
|
53
|
+
<span aria-hidden="true">✨</span>
|
|
54
|
+
<span x-text="label"></span>
|
|
55
|
+
</button>
|
|
56
|
+
</span>
|
|
57
|
+
|
|
58
|
+
<template x-if="open">
|
|
59
|
+
<div class="av-ai-assist-overlay" @click.self="closeModal()" @keydown.escape.window="closeModal()">
|
|
60
|
+
<div class="av-ai-assist-modal" role="dialog" aria-modal="true" aria-label="AI suggestions">
|
|
61
|
+
<div class="av-auth-stack-sm">
|
|
62
|
+
<div class="av-row-between">
|
|
63
|
+
<h3 class="av-card-heading av-m-0">AI Suggestions</h3>
|
|
64
|
+
<button type="button" class="av-btn-ghost av-btn-sm" @click.prevent="closeModal()">Close</button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<template x-if="loading">
|
|
68
|
+
<p class="av-text-soft av-m-0">Generating suggestions...</p>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<template x-if="error && !loading">
|
|
72
|
+
<div class="av-alert av-alert-danger" role="status" x-text="error"></div>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<template x-if="!loading && suggestions.length">
|
|
76
|
+
<div class="av-auth-stack-xs">
|
|
77
|
+
<template x-for="(suggestion, index) in suggestions" :key="`assist-${index}`">
|
|
78
|
+
<div class="av-ai-assist-suggestion">
|
|
79
|
+
<p class="av-m-0" x-text="suggestion"></p>
|
|
80
|
+
<div class="av-row-wrap">
|
|
81
|
+
<button type="button" class="av-btn-ghost av-btn-sm" @click.prevent="appendSuggestion(suggestion)">Append</button>
|
|
82
|
+
<button type="button" class="av-btn-primary av-btn-sm" @click.prevent="replaceSuggestion(suggestion)">Replace</button>
|
|
83
|
+
<button type="button" class="av-btn-ghost av-btn-sm" @click.prevent="copySuggestion(suggestion, index)">
|
|
84
|
+
<span x-text="copiedIndex === index ? 'Copied' : 'Copy'"></span>
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<p class="av-form-hint av-m-0">AI suggestions are optional. Please review before saving.</p>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<script is:inline>
|
|
100
|
+
if (typeof window !== "undefined" && !window.avAiAssist) {
|
|
101
|
+
window.avAiAssist = (config) => ({
|
|
102
|
+
featureKey: config.featureKey,
|
|
103
|
+
valueSourceSelector:
|
|
104
|
+
typeof config.valueSourceSelector === "string" && config.valueSourceSelector.trim()
|
|
105
|
+
? config.valueSourceSelector.trim()
|
|
106
|
+
: null,
|
|
107
|
+
minChars: Number(config.minChars ?? 30),
|
|
108
|
+
maxChars: Number(config.maxChars ?? 1500),
|
|
109
|
+
label: typeof config.label === "string" && config.label ? config.label : "AI",
|
|
110
|
+
onAppendEvent: typeof config.onAppendEvent === "string" && config.onAppendEvent ? config.onAppendEvent : "av:ai-append",
|
|
111
|
+
onReplaceEvent: typeof config.onReplaceEvent === "string" && config.onReplaceEvent ? config.onReplaceEvent : "av:ai-replace",
|
|
112
|
+
value: "",
|
|
113
|
+
open: false,
|
|
114
|
+
loading: false,
|
|
115
|
+
error: null,
|
|
116
|
+
suggestions: [],
|
|
117
|
+
copiedIndex: null,
|
|
118
|
+
observer: null,
|
|
119
|
+
sourceElement: null,
|
|
120
|
+
|
|
121
|
+
init() {
|
|
122
|
+
if (this.valueSourceSelector) {
|
|
123
|
+
this.sourceElement = document.querySelector(this.valueSourceSelector);
|
|
124
|
+
if (this.sourceElement) {
|
|
125
|
+
this.sourceElement.addEventListener("input", () => this.syncValue());
|
|
126
|
+
this.sourceElement.addEventListener("change", () => this.syncValue());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.syncValue();
|
|
131
|
+
|
|
132
|
+
if (typeof MutationObserver !== "undefined") {
|
|
133
|
+
this.observer = new MutationObserver(() => this.syncValue());
|
|
134
|
+
this.observer.observe(this.$el, { attributes: true, attributeFilter: ["data-value"] });
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
get textValue() {
|
|
139
|
+
const raw = typeof this.value === "string" ? this.value : "";
|
|
140
|
+
return raw.trim();
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
get canFetch() {
|
|
144
|
+
return !this.loading && !this.disabledReason;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
get disabledReason() {
|
|
148
|
+
if (this.loading) return "Generating suggestions...";
|
|
149
|
+
if (this.textValue.length > this.maxChars) return `Text exceeds ${this.maxChars} characters.`;
|
|
150
|
+
if (this.textValue.length < this.minChars) return `Add at least ${this.minChars} characters to get meaningful suggestions.`;
|
|
151
|
+
return "";
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
syncValue() {
|
|
155
|
+
if (this.sourceElement && typeof this.sourceElement.value === "string") {
|
|
156
|
+
this.value = this.sourceElement.value;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const nextValue = this.$el.getAttribute("data-value") ?? "";
|
|
161
|
+
this.value = String(nextValue);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
closeModal() {
|
|
165
|
+
this.open = false;
|
|
166
|
+
this.loading = false;
|
|
167
|
+
this.error = null;
|
|
168
|
+
this.suggestions = [];
|
|
169
|
+
this.copiedIndex = null;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
mapError(status, payload) {
|
|
173
|
+
if (status === 401) return "Please sign in again.";
|
|
174
|
+
if (status === 429) return "Too many requests. Try again in a few minutes.";
|
|
175
|
+
if (status === 400) {
|
|
176
|
+
if (payload && typeof payload.error === "string" && payload.error.trim()) {
|
|
177
|
+
return payload.error.trim();
|
|
178
|
+
}
|
|
179
|
+
return "Invalid request.";
|
|
180
|
+
}
|
|
181
|
+
return "Something went wrong. Please try again.";
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async openModal() {
|
|
185
|
+
this.syncValue();
|
|
186
|
+
if (!this.canFetch) return;
|
|
187
|
+
|
|
188
|
+
this.open = true;
|
|
189
|
+
this.loading = true;
|
|
190
|
+
this.error = null;
|
|
191
|
+
this.suggestions = [];
|
|
192
|
+
this.copiedIndex = null;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch("/api/ai/suggest", {
|
|
196
|
+
method: "POST",
|
|
197
|
+
credentials: "include",
|
|
198
|
+
headers: {
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
featureKey: this.featureKey,
|
|
203
|
+
userText: this.textValue,
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const text = await response.text();
|
|
208
|
+
let payload = null;
|
|
209
|
+
|
|
210
|
+
if (text) {
|
|
211
|
+
try {
|
|
212
|
+
payload = JSON.parse(text);
|
|
213
|
+
} catch {
|
|
214
|
+
payload = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
this.error = this.mapError(response.status, payload);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const list = Array.isArray(payload?.suggestions)
|
|
224
|
+
? payload.suggestions
|
|
225
|
+
.filter((item) => typeof item === "string")
|
|
226
|
+
.map((item) => item.trim())
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
: [];
|
|
229
|
+
|
|
230
|
+
this.suggestions = list.slice(0, 5);
|
|
231
|
+
if (!this.suggestions.length) {
|
|
232
|
+
this.error = "No suggestions returned.";
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
this.error = "Something went wrong. Please try again.";
|
|
236
|
+
} finally {
|
|
237
|
+
this.loading = false;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
appendSuggestion(text) {
|
|
242
|
+
const value = String(text ?? "").trim();
|
|
243
|
+
if (!value) return;
|
|
244
|
+
window.dispatchEvent(new CustomEvent(this.onAppendEvent, { detail: { text: value } }));
|
|
245
|
+
this.closeModal();
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
replaceSuggestion(text) {
|
|
249
|
+
const value = String(text ?? "").trim();
|
|
250
|
+
if (!value) return;
|
|
251
|
+
window.dispatchEvent(new CustomEvent(this.onReplaceEvent, { detail: { text: value } }));
|
|
252
|
+
this.closeModal();
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
async copySuggestion(text, index) {
|
|
256
|
+
const value = String(text ?? "").trim();
|
|
257
|
+
if (!value) return;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
if (!navigator?.clipboard?.writeText) {
|
|
261
|
+
throw new Error("Clipboard unavailable");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await navigator.clipboard.writeText(value);
|
|
265
|
+
this.copiedIndex = index;
|
|
266
|
+
window.setTimeout(() => {
|
|
267
|
+
if (this.copiedIndex === index) this.copiedIndex = null;
|
|
268
|
+
}, 1200);
|
|
269
|
+
} catch {
|
|
270
|
+
this.error = "Copy failed. Please copy manually.";
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
<style>
|
|
278
|
+
.av-ai-assist-trigger {
|
|
279
|
+
display: inline-flex;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.av-ai-assist-btn[disabled] {
|
|
283
|
+
cursor: not-allowed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.av-ai-assist-overlay {
|
|
287
|
+
position: fixed;
|
|
288
|
+
inset: 0;
|
|
289
|
+
z-index: 60;
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
justify-content: center;
|
|
293
|
+
padding: 1rem;
|
|
294
|
+
background: rgba(2, 6, 23, 0.72);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.av-ai-assist-modal {
|
|
298
|
+
width: min(720px, 100%);
|
|
299
|
+
max-height: min(80vh, 720px);
|
|
300
|
+
overflow: auto;
|
|
301
|
+
border-radius: 1rem;
|
|
302
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
303
|
+
background: var(--av-surface, #070f23);
|
|
304
|
+
padding: 1rem;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.av-ai-assist-suggestion {
|
|
308
|
+
display: grid;
|
|
309
|
+
gap: 0.75rem;
|
|
310
|
+
border-radius: 0.75rem;
|
|
311
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
312
|
+
background: var(--av-surface-soft, rgba(255, 255, 255, 0.04));
|
|
313
|
+
padding: 0.75rem;
|
|
314
|
+
}
|
|
315
|
+
</style>
|