@ansiversa/components 0.0.165 → 0.0.167

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ansiversa/components",
3
- "version": "0.0.165",
3
+ "version": "0.0.167",
4
4
  "description": "Shared UI components and layouts for the Ansiversa ecosystem",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,54 +1,71 @@
1
1
  ---
2
+ type AssistMode = "suggestions" | "rewrite";
3
+
2
4
  interface Props {
3
- featureKey: string;
4
- value: string;
5
+ mode?: AssistMode;
6
+ featureKey?: string;
7
+ value?: string;
8
+ text?: string;
5
9
  valueSourceSelector?: string;
6
10
  minChars?: number;
7
11
  maxChars?: number;
8
12
  label?: string;
13
+ rewriteField?: "summary" | "generic" | string;
14
+ gatewayOp?: string;
15
+ tone?: "professional" | string;
9
16
  onAppendEvent?: string;
10
17
  onReplaceEvent?: string;
11
18
  [key: string]: any;
12
19
  }
13
20
 
14
21
  const {
22
+ mode = "suggestions",
15
23
  featureKey,
16
24
  value,
25
+ text,
17
26
  valueSourceSelector,
18
27
  minChars = 30,
19
28
  maxChars = 1500,
20
29
  label = "AI",
30
+ rewriteField = "generic",
31
+ gatewayOp = "rewrite",
32
+ tone = "professional",
21
33
  onAppendEvent = "av:ai-append",
22
34
  onReplaceEvent = "av:ai-replace",
23
35
  ...rest
24
36
  } = Astro.props as Props;
25
37
 
26
38
  const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
39
+ const initialValue = typeof text === "string" ? text : (value ?? "");
27
40
  ---
28
41
 
29
42
  <div
30
43
  class="av-ai-assist"
31
44
  data-av-ai-assist={componentId}
32
- data-value={value}
45
+ data-value={initialValue}
33
46
  {...rest}
34
47
  x-data={`avAiAssist(${JSON.stringify({
35
- featureKey,
48
+ mode,
49
+ featureKey: featureKey ?? null,
36
50
  valueSourceSelector: valueSourceSelector ?? null,
37
51
  minChars,
38
52
  maxChars,
39
53
  label,
54
+ rewriteField,
55
+ gatewayOp,
56
+ tone,
40
57
  onAppendEvent,
41
58
  onReplaceEvent,
42
59
  })})`}
43
60
  x-init="init()"
44
61
  >
45
- <span class="av-ai-assist-trigger" :title="disabledReason || `Get ${label} suggestions`">
62
+ <span class="av-ai-assist-trigger" :title="disabledReason || buttonTitle">
46
63
  <button
47
64
  type="button"
48
65
  class="av-btn-ghost av-btn-sm av-ai-assist-btn"
49
66
  @click.prevent="openModal()"
50
67
  :disabled="!canFetch"
51
- aria-label="Get AI suggestions"
68
+ :aria-label="buttonAriaLabel"
52
69
  >
53
70
  <span aria-hidden="true">✨</span>
54
71
  <span x-text="label"></span>
@@ -57,15 +74,15 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
57
74
 
58
75
  <template x-if="open">
59
76
  <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">
77
+ <div class="av-ai-assist-modal" role="dialog" aria-modal="true" :aria-label="modalAriaLabel">
61
78
  <div class="av-auth-stack-sm">
62
79
  <div class="av-ai-assist-modal-header">
63
- <h3 class="av-card-heading av-m-0">AI Suggestions</h3>
80
+ <h3 class="av-card-heading av-m-0" x-text="modalTitle"></h3>
64
81
  <button
65
82
  type="button"
66
83
  class="av-btn-ghost av-btn-sm av-ai-assist-close"
67
84
  @click.prevent="closeModal()"
68
- aria-label="Close AI suggestions"
85
+ :aria-label="closeAriaLabel"
69
86
  title="Close"
70
87
  >
71
88
  <span aria-hidden="true">X</span>
@@ -73,14 +90,24 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
73
90
  </div>
74
91
 
75
92
  <template x-if="loading">
76
- <p class="av-text-soft av-m-0">Generating suggestions...</p>
93
+ <p class="av-text-soft av-m-0" x-text="loadingLabel"></p>
77
94
  </template>
78
95
 
79
96
  <template x-if="error && !loading">
80
97
  <div class="av-alert av-alert-danger" role="status" x-text="error"></div>
81
98
  </template>
82
99
 
83
- <template x-if="!loading && suggestions.length">
100
+ <template x-if="isRewriteMode() && !loading && rewrittenText">
101
+ <div class="av-auth-stack-xs">
102
+ <textarea class="av-input av-ai-assist-rewrite-output" readonly x-text="rewrittenText"></textarea>
103
+ <div class="av-row-wrap">
104
+ <button type="button" class="av-btn-primary av-btn-sm" @click.prevent="replaceSuggestion(rewrittenText)">Replace</button>
105
+ <button type="button" class="av-btn-ghost av-btn-sm" @click.prevent="closeModal()">Close</button>
106
+ </div>
107
+ </div>
108
+ </template>
109
+
110
+ <template x-if="!isRewriteMode() && !loading && suggestions.length">
84
111
  <div class="av-auth-stack-xs">
85
112
  <template x-for="(suggestion, index) in suggestions" :key="`assist-${index}`">
86
113
  <div class="av-ai-assist-suggestion">
@@ -97,7 +124,7 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
97
124
  </div>
98
125
  </template>
99
126
 
100
- <p class="av-form-hint av-m-0">AI suggestions are optional. Please review before saving.</p>
127
+ <p class="av-form-hint av-m-0" x-text="footerHint"></p>
101
128
  </div>
102
129
  </div>
103
130
  </div>
@@ -107,7 +134,8 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
107
134
  <script is:inline>
108
135
  if (typeof window !== "undefined" && !window.avAiAssist) {
109
136
  window.avAiAssist = (config) => ({
110
- featureKey: config.featureKey,
137
+ mode: config.mode === "rewrite" ? "rewrite" : "suggestions",
138
+ featureKey: typeof config.featureKey === "string" ? config.featureKey : null,
111
139
  valueSourceSelector:
112
140
  typeof config.valueSourceSelector === "string" && config.valueSourceSelector.trim()
113
141
  ? config.valueSourceSelector.trim()
@@ -115,6 +143,12 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
115
143
  minChars: Number(config.minChars ?? 30),
116
144
  maxChars: Number(config.maxChars ?? 1500),
117
145
  label: typeof config.label === "string" && config.label ? config.label : "AI",
146
+ rewriteField:
147
+ typeof config.rewriteField === "string" && config.rewriteField.trim()
148
+ ? config.rewriteField.trim()
149
+ : "generic",
150
+ gatewayOp: typeof config.gatewayOp === "string" && config.gatewayOp ? config.gatewayOp : "rewrite",
151
+ tone: typeof config.tone === "string" && config.tone ? config.tone : "professional",
118
152
  onAppendEvent: typeof config.onAppendEvent === "string" && config.onAppendEvent ? config.onAppendEvent : "av:ai-append",
119
153
  onReplaceEvent: typeof config.onReplaceEvent === "string" && config.onReplaceEvent ? config.onReplaceEvent : "av:ai-replace",
120
154
  value: "",
@@ -122,6 +156,7 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
122
156
  loading: false,
123
157
  error: null,
124
158
  suggestions: [],
159
+ rewrittenText: "",
125
160
  copiedIndex: null,
126
161
  observer: null,
127
162
  sourceElement: null,
@@ -143,6 +178,10 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
143
178
  }
144
179
  },
145
180
 
181
+ isRewriteMode() {
182
+ return this.mode === "rewrite";
183
+ },
184
+
146
185
  get textValue() {
147
186
  const raw = typeof this.value === "string" ? this.value : "";
148
187
  return raw.trim();
@@ -152,8 +191,42 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
152
191
  return !this.loading && !this.disabledReason;
153
192
  },
154
193
 
194
+ get buttonTitle() {
195
+ return this.isRewriteMode() ? "Rewrite with AI" : `Get ${this.label} suggestions`;
196
+ },
197
+
198
+ get buttonAriaLabel() {
199
+ return this.isRewriteMode() ? "Rewrite with AI" : "Get AI suggestions";
200
+ },
201
+
202
+ get modalTitle() {
203
+ return this.isRewriteMode() ? "AI Rewrite" : "AI Suggestions";
204
+ },
205
+
206
+ get modalAriaLabel() {
207
+ return this.isRewriteMode() ? "AI rewrite" : "AI suggestions";
208
+ },
209
+
210
+ get closeAriaLabel() {
211
+ return this.isRewriteMode() ? "Close AI rewrite" : "Close AI suggestions";
212
+ },
213
+
214
+ get loadingLabel() {
215
+ return this.isRewriteMode() ? "Rewriting text..." : "Generating suggestions...";
216
+ },
217
+
218
+ get footerHint() {
219
+ return this.isRewriteMode()
220
+ ? "AI rewrite is optional. Please review before saving."
221
+ : "AI suggestions are optional. Please review before saving.";
222
+ },
223
+
155
224
  get disabledReason() {
156
- if (this.loading) return "Generating suggestions...";
225
+ if (this.loading) return this.loadingLabel;
226
+ if (this.isRewriteMode()) {
227
+ if (this.textValue.length === 0) return "Add text to rewrite.";
228
+ return "";
229
+ }
157
230
  if (this.textValue.length > this.maxChars) return `Text exceeds ${this.maxChars} characters.`;
158
231
  if (this.textValue.length < this.minChars) return `Add at least ${this.minChars} characters to get meaningful suggestions.`;
159
232
  return "";
@@ -174,6 +247,7 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
174
247
  this.loading = false;
175
248
  this.error = null;
176
249
  this.suggestions = [];
250
+ this.rewrittenText = "";
177
251
  this.copiedIndex = null;
178
252
  },
179
253
 
@@ -197,19 +271,30 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
197
271
  this.loading = true;
198
272
  this.error = null;
199
273
  this.suggestions = [];
274
+ this.rewrittenText = "";
200
275
  this.copiedIndex = null;
201
276
 
202
277
  try {
278
+ const body = this.isRewriteMode()
279
+ ? {
280
+ op: this.gatewayOp || "rewrite",
281
+ text: this.textValue,
282
+ field: this.rewriteField || "generic",
283
+ maxChars: Number.isFinite(this.maxChars) ? this.maxChars : undefined,
284
+ tone: this.tone || "professional",
285
+ }
286
+ : {
287
+ featureKey: this.featureKey,
288
+ userText: this.textValue,
289
+ };
290
+
203
291
  const response = await fetch("/api/ai/suggest", {
204
292
  method: "POST",
205
293
  credentials: "include",
206
294
  headers: {
207
295
  "Content-Type": "application/json",
208
296
  },
209
- body: JSON.stringify({
210
- featureKey: this.featureKey,
211
- userText: this.textValue,
212
- }),
297
+ body: JSON.stringify(body),
213
298
  });
214
299
 
215
300
  const text = await response.text();
@@ -228,6 +313,15 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
228
313
  return;
229
314
  }
230
315
 
316
+ if (this.isRewriteMode()) {
317
+ const rewritten = typeof payload?.text === "string" ? payload.text.trim() : "";
318
+ this.rewrittenText = rewritten;
319
+ if (!this.rewrittenText) {
320
+ this.error = "No rewrite returned.";
321
+ }
322
+ return;
323
+ }
324
+
231
325
  const list = Array.isArray(payload?.suggestions)
232
326
  ? payload.suggestions
233
327
  .filter((item) => typeof item === "string")
@@ -335,4 +429,9 @@ const componentId = `av-ai-assist-${Math.random().toString(36).slice(2, 10)}`;
335
429
  background: var(--av-surface-soft, rgba(255, 255, 255, 0.04));
336
430
  padding: 0.75rem;
337
431
  }
432
+
433
+ .av-ai-assist-rewrite-output {
434
+ min-height: 10rem;
435
+ resize: none;
436
+ }
338
437
  </style>
@@ -151,7 +151,7 @@ const hasDeclaration = Boolean(declaration.text || declaration.place || declarat
151
151
  return (
152
152
  <div class="av-print-avoid-break flex flex-col gap-1 sm:flex-row sm:items-baseline sm:justify-between">
153
153
  <div>
154
- {edu.degree ? <h3 class="text-base font-semibold">{edu.degree}</h3> : null}
154
+ {edu.degree ? <h3 class="text-base font-semibold text-slate-900">{edu.degree}</h3> : null}
155
155
  <p class="text-sm text-slate-700">{edu.school}</p>
156
156
  {edu.location && (
157
157
  <p class="text-sm text-slate-600">{edu.location}</p>