yaxa-svelte 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -12
- package/dist/admin/components/AdminDashboard.svelte +6 -0
- package/dist/admin/components/DevSandbox.svelte +6 -2
- package/dist/admin/components/RecordDrawer.svelte +4 -0
- package/dist/admin/handler.js +13 -13
- package/dist/components/ai/AiChat.svelte +263 -0
- package/dist/components/ai/AiChat.svelte.d.ts +41 -0
- package/dist/components/ai/AiThought.svelte +108 -0
- package/dist/components/ai/AiThought.svelte.d.ts +12 -0
- package/dist/components/ai/AiToolCall.svelte +133 -0
- package/dist/components/ai/AiToolCall.svelte.d.ts +13 -0
- package/dist/components/ai/PromptBar.svelte +279 -0
- package/dist/components/ai/PromptBar.svelte.d.ts +22 -0
- package/dist/components/ai/ai.test.d.ts +1 -0
- package/dist/components/ai/ai.test.js +13 -0
- package/dist/components/ai/index.d.ts +8 -0
- package/dist/components/ai/index.js +4 -0
- package/dist/components/charts/BarChart.svelte +248 -0
- package/dist/components/charts/BarChart.svelte.d.ts +22 -0
- package/dist/components/charts/DonutChart.svelte +153 -0
- package/dist/components/charts/DonutChart.svelte.d.ts +18 -0
- package/dist/components/charts/LineChart.svelte +334 -0
- package/dist/components/charts/LineChart.svelte.d.ts +28 -0
- package/dist/components/charts/charts.test.d.ts +1 -0
- package/dist/components/charts/charts.test.js +11 -0
- package/dist/components/charts/index.d.ts +6 -0
- package/dist/components/charts/index.js +3 -0
- package/dist/components/elements/CodeBlock.svelte +86 -58
- package/dist/components/elements/CodeBlock.svelte.d.ts +2 -1
- package/dist/components/elements/DataTable.svelte +363 -144
- package/dist/components/elements/DataTable.svelte.d.ts +15 -2
- package/dist/components/elements/Icon.svelte +32 -1
- package/dist/components/forms/Checkbox.svelte +3 -0
- package/dist/components/forms/Checkbox.svelte.d.ts +1 -0
- package/dist/components/forms/ColorPicker.svelte +22 -2
- package/dist/components/forms/ColorPicker.svelte.d.ts +5 -0
- package/dist/components/forms/Combobox.svelte +3 -0
- package/dist/components/forms/CreditCardInput.svelte +167 -0
- package/dist/components/forms/CreditCardInput.svelte.d.ts +14 -0
- package/dist/components/forms/Dropzone.svelte +11 -0
- package/dist/components/forms/Dropzone.svelte.d.ts +4 -0
- package/dist/components/forms/FormField.svelte +6 -1
- package/dist/components/forms/FormField.svelte.d.ts +1 -0
- package/dist/components/forms/InputOTP.svelte +6 -0
- package/dist/components/forms/InputOTP.svelte.d.ts +2 -0
- package/dist/components/forms/MultiSelect.svelte +3 -0
- package/dist/components/forms/NumberInput.svelte +11 -1
- package/dist/components/forms/NumberInput.svelte.d.ts +4 -0
- package/dist/components/forms/PhoneInput.svelte +140 -0
- package/dist/components/forms/PhoneInput.svelte.d.ts +18 -0
- package/dist/components/forms/RadioGroup.svelte +2 -0
- package/dist/components/forms/RichTextEditor.svelte +265 -0
- package/dist/components/forms/RichTextEditor.svelte.d.ts +11 -0
- package/dist/components/forms/Select.svelte +14 -1
- package/dist/components/forms/Select.svelte.d.ts +5 -0
- package/dist/components/forms/Slider.svelte +14 -1
- package/dist/components/forms/Slider.svelte.d.ts +4 -0
- package/dist/components/forms/Switch.svelte +14 -1
- package/dist/components/forms/Switch.svelte.d.ts +3 -0
- package/dist/components/forms/advanced-forms.test.d.ts +1 -0
- package/dist/components/forms/advanced-forms.test.js +23 -0
- package/dist/components/layout/DashboardShell.svelte +6 -4
- package/dist/components/layout/DashboardShell.svelte.d.ts +1 -0
- package/dist/components/layout/Footer.svelte +10 -15
- package/dist/components/layout/Header.svelte +5 -4
- package/dist/components/layout/YaxaApp.svelte +9 -7
- package/dist/components/legal/LegalDocument.svelte +3 -2
- package/dist/components/navigation/CommandPalette.svelte +3 -0
- package/dist/components/navigation/DropdownMenu.svelte +14 -11
- package/dist/components/overlays/Popover.svelte +21 -4
- package/dist/components/overlays/Popover.svelte.d.ts +2 -0
- package/dist/components/overlays/Tooltip.svelte +28 -8
- package/dist/components/overlays/Tooltip.svelte.d.ts +3 -0
- package/dist/components/saas/Gate.svelte +3 -2
- package/dist/components/saas/UserMenu.svelte +3 -3
- package/dist/components/seo/Favicons.svelte +37 -12
- package/dist/components/seo/Favicons.svelte.d.ts +4 -0
- package/dist/components/seo/OgImage.svelte +3 -2
- package/dist/components/seo/Seo.svelte +6 -4
- package/dist/components/theme/ThemeStudio.svelte +411 -0
- package/dist/components/theme/ThemeStudio.svelte.d.ts +6 -0
- package/dist/composables/useGate.svelte.js +3 -2
- package/dist/composables/useLocale.svelte.js +8 -6
- package/dist/index.d.ts +24 -2
- package/dist/index.js +16 -3
- package/dist/site/config.d.ts +2 -0
- package/dist/site/context.d.ts +1 -30
- package/dist/site/context.js +1 -43
- package/dist/site/context.svelte.d.ts +37 -0
- package/dist/site/context.svelte.js +52 -0
- package/dist/site/context.test.d.ts +1 -0
- package/dist/site/context.test.js +29 -0
- package/dist/site/manifest.js +6 -5
- package/dist/styles/yaxa.css +102 -4
- package/dist/theme/theme.svelte.d.ts +6 -1
- package/dist/theme/theme.svelte.js +49 -0
- package/dist/vite/index.js +12 -4
- package/dist/vite/index.test.js +6 -6
- package/package.json +8 -12
package/README.md
CHANGED
|
@@ -23,13 +23,16 @@ _Nuxt UI v4 & Nuxt UI Pro Equivalent for SvelteKit 2.7+ & Svelte 5 with Built-in
|
|
|
23
23
|
|
|
24
24
|
**Yaxa** brings the full developer experience, visual elegance, and full-stack toolkit of **Nuxt UI** and the **Nuxt SEO + SaaS ecosystem** to **SvelteKit 2.7+** and **Svelte 5**.
|
|
25
25
|
|
|
26
|
-
- 🎨 **Tailwind CSS v4 Native Tokens**: Styled with `@theme` variables;
|
|
26
|
+
- 🎨 **Tailwind CSS v4 Native Tokens**: Styled with `@theme` variables; harmonic radius scale (`--radius-xs` to `--radius-4xl`), chart palette tokens (`--color-chart-1` to `--color-chart-5`), and auto-adaptive scrollbars.
|
|
27
|
+
- 🎛️ **Visual Web Theme Studio**: Interactive runtime palette designer (`/theme` and `<ThemeStudio />`) with live sandbox preview and 1-click token export.
|
|
28
|
+
- 🤖 **AI-Native Primitives**: Drop-in `<AiChat>`, `<PromptBar>`, `<AiThought>`, and `<AiToolCall>` for conversational interfaces and agentic workflows.
|
|
29
|
+
- 📊 **Zero-Dependency SVG Charts**: Pure Svelte 5 `<LineChart>`, `<BarChart>`, and `<DonutChart>` with responsive tooltips, curved splines, and theme-palette synchronisation.
|
|
27
30
|
- ⚡ **Pure Svelte 5 Runes**: Built using `$state`, `$derived`, `$props`, and `$bindable` — zero Virtual DOM overhead.
|
|
28
31
|
- ♿ **Accessible by Default**: Headless primitives powered by **Bits UI**.
|
|
29
32
|
- 🚀 **100% DRY SEO Parity**: Automate dynamic Open Graph cards (`/api/og`), environment-aware `robots.txt`, XML sitemaps with human-readable `sitemap.xsl` stylesheets, PWA manifests, and Schema.org JSON-LD from a single `src/site.config.ts`.
|
|
30
33
|
- 🔐 **Batteries-Included SaaS Suite**: Pre-integrated **Better-Auth**, multi-dialect **Drizzle ORM** (Neon PostgreSQL & Turso LibSQL/SQLite), **Polar.sh** payments & webhook sync, and **Resend** transactional emails.
|
|
31
|
-
- 🧩 **
|
|
32
|
-
- 🛠️ **Composable Runes**: `useAuth`, `useShortcuts`, `useClipboard`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`.
|
|
34
|
+
- 🧩 **45+ Production Components**: Elements, AI primitives, charts, advanced forms (`<RichTextEditor>`, `<PhoneInput>`, `<CreditCardInput>`), layout, overlays, and drop-in SaaS widgets (`<AuthCard>`, `<UserMenu>`, `<PricingTable>`, `<SubscriptionCard>`).
|
|
35
|
+
- 🛠️ **Composable Runes**: `useAuth`, `useShortcuts`, `useClipboard`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`, `useIdle`.
|
|
33
36
|
|
|
34
37
|
---
|
|
35
38
|
|
|
@@ -228,15 +231,18 @@ export const POST = createPolarWebhookHandler();
|
|
|
228
231
|
|
|
229
232
|
## 🧩 Component Library
|
|
230
233
|
|
|
231
|
-
| Category
|
|
232
|
-
|
|
|
233
|
-
| **
|
|
234
|
-
| **
|
|
235
|
-
| **
|
|
236
|
-
| **
|
|
237
|
-
| **
|
|
238
|
-
| **
|
|
239
|
-
| **
|
|
234
|
+
| Category | Components |
|
|
235
|
+
| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
236
|
+
| **Theming & Studio** | `<ThemeStudio>`, `theme`, `ACCENT_PALETTES`, `NEUTRAL_PALETTES`, `RADIUS_PRESETS`, Harmonic Radius Multiplier Scale, Chart Palette Series Tokens |
|
|
237
|
+
| **AI Primitives** | `<AiChat>`, `<PromptBar>`, `<AiThought>`, `<AiToolCall>` |
|
|
238
|
+
| **Charts (Pure SVG)** | `<LineChart>`, `<BarChart>`, `<DonutChart>` (0 external charting dependencies, auto-responsive, dark/light theme aware) |
|
|
239
|
+
| **SaaS Suite** | `<Gate>`, `<AuthCard>`, `<UserMenu>`, `<OrgSwitcher>`, `<PricingCard>`, `<PricingTable>`, `<SubscriptionCard>`, `useAuth`, `useGate`, `useUpload` |
|
|
240
|
+
| **Admin Suite** | `<AdminDashboard>`, `<RecordDrawer>`, `<DevSandbox>`, `<ImpersonationBanner>`, `createYaxaAdminHook`, `createDrizzleAdmin` |
|
|
241
|
+
| **Elements** | `Button`, `ButtonGroup`, `Badge`, `Avatar`, `AvatarGroup`, `DataTable` (with Column Visibility & Export), `Chip`, `Meter`, `MetricCard`, `Kbd`, `Icon`, `Spinner`, `Progress`, `Skeleton`, `Link`, `Logo`, `CodeBlock` (with Svelte 5 Runes Highlighting), `Tree`, `Timeline`, `Terminal`, `VirtualList`, `Carousel` |
|
|
242
|
+
| **Forms** | `Form`, `FormField`, `Input`, `InputOTP`, `Textarea`, `Checkbox`, `Switch`, `Select`, `Combobox`, `MultiSelect`, `NumberInput`, `RadioGroup`, `Slider`, `ColorPicker`, `ToggleGroup`, `DatePicker`, `DateRangePicker`, `Dropzone`, `<RichTextEditor>`, `<PhoneInput>`, `<CreditCardInput>` |
|
|
243
|
+
| **Layout & Blocks** | `Container`, `Header`, `Footer`, `Section`, `Card`, `Divider`, `YaxaApp`, `DashboardShell` (with independent sidebar tokens), `ResizablePanels`, Page Blocks (`Hero`, `SaaS Metrics`, `FAQ`, `Testimonials`, `FeatureGrid`) |
|
|
244
|
+
| **Overlays & Nav** | `Tabs`, `Breadcrumb`, `Pagination`, `CommandPalette` (`⌘K`), `DropdownMenu`, `ContextMenu`, `Modal`, `Drawer`, `Slideover`, `Popover`, `Tooltip` (with `arrow={false}` & `unstyled`), `Alert`, `Accordion`, `NotificationCenter`, `Stepper` |
|
|
245
|
+
| **Composables** | `useAuth`, `useGate`, `useUpload`, `useClipboard`, `useShortcuts`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`, `useIdle`, `useInfiniteScroll`, `useSortable` |
|
|
240
246
|
|
|
241
247
|
---
|
|
242
248
|
|
|
@@ -284,8 +284,11 @@
|
|
|
284
284
|
<div class="flex items-center gap-2">
|
|
285
285
|
<form onsubmit={handleSearch} class="relative">
|
|
286
286
|
<input
|
|
287
|
+
id="admin-search-input"
|
|
288
|
+
name="adminSearch"
|
|
287
289
|
type="search"
|
|
288
290
|
placeholder="Search records..."
|
|
291
|
+
aria-label="Search records"
|
|
289
292
|
bind:value={searchQuery}
|
|
290
293
|
class="w-48 rounded-lg border border-neutral-300 bg-white py-1.5 pr-3 pl-8 text-xs text-neutral-900 placeholder-neutral-400 transition-all focus:w-60 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
291
294
|
/>
|
|
@@ -424,6 +427,7 @@
|
|
|
424
427
|
{#if col.dataType === 'boolean'}
|
|
425
428
|
<input
|
|
426
429
|
id={`create-${col.name}`}
|
|
430
|
+
name={col.name}
|
|
427
431
|
type="checkbox"
|
|
428
432
|
class="h-4 w-4 rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
|
|
429
433
|
checked={Boolean(newRecordData[col.name])}
|
|
@@ -432,6 +436,7 @@
|
|
|
432
436
|
{:else if col.dataType === 'number' || col.dataType === 'integer'}
|
|
433
437
|
<input
|
|
434
438
|
id={`create-${col.name}`}
|
|
439
|
+
name={col.name}
|
|
435
440
|
type="number"
|
|
436
441
|
placeholder={col.primaryKey ? '(Auto-generated if empty)' : ''}
|
|
437
442
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs dark:border-neutral-700 dark:bg-neutral-900"
|
|
@@ -443,6 +448,7 @@
|
|
|
443
448
|
{:else}
|
|
444
449
|
<input
|
|
445
450
|
id={`create-${col.name}`}
|
|
451
|
+
name={col.name}
|
|
446
452
|
type="text"
|
|
447
453
|
placeholder={col.primaryKey ? '(Auto-generated UUID if empty)' : ''}
|
|
448
454
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs dark:border-neutral-700 dark:bg-neutral-900"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import Icon from '../../components/elements/Icon.svelte';
|
|
6
6
|
import Progress from '../../components/elements/Progress.svelte';
|
|
7
7
|
import { useUpload } from '../../composables/useUpload.svelte';
|
|
8
|
-
import {
|
|
8
|
+
import { useYaxa } from '../../site/context';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
apiPrefix?: string;
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
let { apiPrefix = '/admin/api', class: className = '' }: Props = $props();
|
|
16
16
|
|
|
17
17
|
// Auth & Context info
|
|
18
|
-
const
|
|
18
|
+
const yaxa = useYaxa();
|
|
19
|
+
const user = $derived(yaxa.user);
|
|
19
20
|
|
|
20
21
|
// Email Sandbox State
|
|
21
22
|
let emailTo = $state('developer@example.com');
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
>
|
|
97
98
|
<input
|
|
98
99
|
id="email-to"
|
|
100
|
+
name="emailTo"
|
|
99
101
|
type="email"
|
|
100
102
|
bind:value={emailTo}
|
|
101
103
|
placeholder="you@example.com"
|
|
@@ -109,6 +111,7 @@
|
|
|
109
111
|
>
|
|
110
112
|
<select
|
|
111
113
|
id="email-template"
|
|
114
|
+
name="emailTemplate"
|
|
112
115
|
bind:value={emailTemplate}
|
|
113
116
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs text-neutral-900 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
114
117
|
>
|
|
@@ -172,6 +175,7 @@
|
|
|
172
175
|
>
|
|
173
176
|
<input
|
|
174
177
|
id="storage-file"
|
|
178
|
+
name="storageFile"
|
|
175
179
|
type="file"
|
|
176
180
|
onchange={(e) => {
|
|
177
181
|
const files = e.currentTarget.files;
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
<div class="flex items-center gap-2 pt-1">
|
|
139
139
|
<input
|
|
140
140
|
id={`field-${col.name}`}
|
|
141
|
+
name={col.name}
|
|
141
142
|
type="checkbox"
|
|
142
143
|
class="h-4 w-4 rounded border-neutral-300 text-primary-600 focus:ring-primary-500 dark:border-neutral-700 dark:bg-neutral-800"
|
|
143
144
|
checked={Boolean(formData[col.name])}
|
|
@@ -150,6 +151,7 @@
|
|
|
150
151
|
{:else if col.dataType === 'number' || col.dataType === 'integer'}
|
|
151
152
|
<input
|
|
152
153
|
id={`field-${col.name}`}
|
|
154
|
+
name={col.name}
|
|
153
155
|
type="number"
|
|
154
156
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm text-neutral-900 placeholder-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
155
157
|
disabled={col.primaryKey}
|
|
@@ -162,6 +164,7 @@
|
|
|
162
164
|
{:else if String(formData[col.name] ?? '').length > 60 || col.name.includes('description') || col.name.includes('body')}
|
|
163
165
|
<textarea
|
|
164
166
|
id={`field-${col.name}`}
|
|
167
|
+
name={col.name}
|
|
165
168
|
rows={3}
|
|
166
169
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 font-mono text-sm text-xs text-neutral-900 placeholder-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
167
170
|
disabled={col.primaryKey}
|
|
@@ -172,6 +175,7 @@
|
|
|
172
175
|
{:else}
|
|
173
176
|
<input
|
|
174
177
|
id={`field-${col.name}`}
|
|
178
|
+
name={col.name}
|
|
175
179
|
type="text"
|
|
176
180
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm text-neutral-900 placeholder-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
177
181
|
disabled={col.primaryKey}
|
package/dist/admin/handler.js
CHANGED
|
@@ -16,17 +16,17 @@ export function introspectTables(schema) {
|
|
|
16
16
|
const columns = [];
|
|
17
17
|
let primaryKey = 'id';
|
|
18
18
|
for (const [colKey, col] of Object.entries(rawColumns)) {
|
|
19
|
-
const
|
|
20
|
-
const isPk = Boolean(
|
|
19
|
+
const colTyped = col;
|
|
20
|
+
const isPk = Boolean(colTyped.primary || colTyped.isPrimaryKey);
|
|
21
21
|
if (isPk) {
|
|
22
|
-
primaryKey =
|
|
22
|
+
primaryKey = colTyped.name || colKey;
|
|
23
23
|
}
|
|
24
24
|
columns.push({
|
|
25
|
-
name:
|
|
26
|
-
dataType:
|
|
25
|
+
name: colTyped.name || colKey,
|
|
26
|
+
dataType: colTyped.dataType || colTyped.columnType || typeof colTyped,
|
|
27
27
|
primaryKey: isPk,
|
|
28
|
-
notNull: Boolean(
|
|
29
|
-
hasDefault: Boolean(
|
|
28
|
+
notNull: Boolean(colTyped.notNull),
|
|
29
|
+
hasDefault: Boolean(colTyped.hasDefault || colTyped.default !== undefined)
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
const info = {
|
|
@@ -98,13 +98,13 @@ export class DrizzleAdminService {
|
|
|
98
98
|
const searchTerm = `%${options.search.trim()}%`;
|
|
99
99
|
const textColFilters = [];
|
|
100
100
|
for (const [, col] of Object.entries(rawColumns)) {
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
const colTyped = col;
|
|
102
|
+
if (colTyped.dataType === 'string' ||
|
|
103
|
+
colTyped.dataType === 'text' ||
|
|
104
|
+
colTyped.columnType?.includes('Text') ||
|
|
105
|
+
colTyped.columnType?.includes('Varchar')) {
|
|
106
106
|
try {
|
|
107
|
-
textColFilters.push(like(
|
|
107
|
+
textColFilters.push(like(col, searchTerm));
|
|
108
108
|
}
|
|
109
109
|
catch {
|
|
110
110
|
// fallback
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
export interface AiMessageThought {
|
|
5
|
+
duration?: number;
|
|
6
|
+
running?: boolean;
|
|
7
|
+
text?: string;
|
|
8
|
+
steps?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AiMessageTool {
|
|
12
|
+
name: string;
|
|
13
|
+
status?: 'idle' | 'running' | 'success' | 'error';
|
|
14
|
+
args?: Record<string, any> | string;
|
|
15
|
+
result?: Record<string, any> | string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AiMessage {
|
|
19
|
+
id: string;
|
|
20
|
+
role: 'user' | 'assistant' | 'system';
|
|
21
|
+
content: string;
|
|
22
|
+
createdAt?: Date | string;
|
|
23
|
+
thought?: AiMessageThought;
|
|
24
|
+
tools?: AiMessageTool[];
|
|
25
|
+
isStreaming?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AiChatProps {
|
|
29
|
+
messages?: AiMessage[];
|
|
30
|
+
loading?: boolean;
|
|
31
|
+
userAvatar?: string;
|
|
32
|
+
assistantAvatar?: string;
|
|
33
|
+
assistantName?: string;
|
|
34
|
+
placeholder?: string;
|
|
35
|
+
autoScroll?: boolean;
|
|
36
|
+
class?: string;
|
|
37
|
+
onsend?: (text: string, files?: File[]) => void;
|
|
38
|
+
onstop?: () => void;
|
|
39
|
+
onretry?: (message: AiMessage) => void;
|
|
40
|
+
header?: Snippet;
|
|
41
|
+
messageSlot?: Snippet<[AiMessage]>;
|
|
42
|
+
footer?: Snippet;
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<script lang="ts">
|
|
47
|
+
import Avatar from '../elements/Avatar.svelte';
|
|
48
|
+
import Icon from '../elements/Icon.svelte';
|
|
49
|
+
import PromptBar from './PromptBar.svelte';
|
|
50
|
+
import AiThought from './AiThought.svelte';
|
|
51
|
+
import AiToolCall from './AiToolCall.svelte';
|
|
52
|
+
import { useToast } from '../../composables/useToast';
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
messages = [],
|
|
56
|
+
loading = false,
|
|
57
|
+
userAvatar = '',
|
|
58
|
+
assistantAvatar = '',
|
|
59
|
+
assistantName = 'AI Assistant',
|
|
60
|
+
placeholder = 'Ask a question or type / for commands...',
|
|
61
|
+
autoScroll = true,
|
|
62
|
+
class: className = '',
|
|
63
|
+
onsend,
|
|
64
|
+
onstop,
|
|
65
|
+
onretry,
|
|
66
|
+
header,
|
|
67
|
+
messageSlot,
|
|
68
|
+
footer
|
|
69
|
+
}: AiChatProps = $props();
|
|
70
|
+
|
|
71
|
+
let scrollContainer: HTMLDivElement | null = $state(null);
|
|
72
|
+
let promptValue = $state('');
|
|
73
|
+
const toast = useToast();
|
|
74
|
+
|
|
75
|
+
function scrollToBottom(smooth = true) {
|
|
76
|
+
if (scrollContainer && autoScroll) {
|
|
77
|
+
scrollContainer.scrollTo({
|
|
78
|
+
top: scrollContainer.scrollHeight,
|
|
79
|
+
behavior: smooth ? 'smooth' : 'auto'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
$effect(() => {
|
|
85
|
+
if (messages.length > 0) {
|
|
86
|
+
setTimeout(() => scrollToBottom(true), 50);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function copyMessage(text: string) {
|
|
91
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
92
|
+
navigator.clipboard.writeText(text);
|
|
93
|
+
toast.success('Copied to clipboard');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<div
|
|
99
|
+
class="flex h-full min-h-[500px] w-full flex-col overflow-hidden rounded-2xl border border-neutral-200 bg-white shadow-md dark:border-neutral-800 dark:bg-neutral-950 {className}"
|
|
100
|
+
>
|
|
101
|
+
<!-- Chat Header Bar -->
|
|
102
|
+
<div
|
|
103
|
+
class="flex items-center justify-between border-b border-neutral-200/80 bg-neutral-50/50 px-4 py-3 dark:border-neutral-800/80 dark:bg-neutral-900/50"
|
|
104
|
+
>
|
|
105
|
+
{#if header}
|
|
106
|
+
{@render header()}
|
|
107
|
+
{:else}
|
|
108
|
+
<div class="flex items-center gap-3">
|
|
109
|
+
<div
|
|
110
|
+
class="flex h-8 w-8 items-center justify-center rounded-xl bg-primary-100 text-primary-600 dark:bg-primary-950 dark:text-primary-400"
|
|
111
|
+
>
|
|
112
|
+
<Icon name="bot" size="sm" />
|
|
113
|
+
</div>
|
|
114
|
+
<div>
|
|
115
|
+
<div class="text-sm font-semibold text-neutral-900 dark:text-white">
|
|
116
|
+
{assistantName}
|
|
117
|
+
</div>
|
|
118
|
+
<div class="flex items-center gap-1.5 text-[11px] text-neutral-500">
|
|
119
|
+
<span
|
|
120
|
+
class="h-1.5 w-1.5 rounded-full {loading
|
|
121
|
+
? 'animate-ping bg-amber-500'
|
|
122
|
+
: 'bg-emerald-500'}"
|
|
123
|
+
></span>
|
|
124
|
+
<span>{loading ? 'Generating response...' : 'Online & Ready'}</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
{/if}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<!-- Scrollable Messages Container -->
|
|
132
|
+
<div
|
|
133
|
+
bind:this={scrollContainer}
|
|
134
|
+
class="yaxa-scrollbar flex-1 space-y-6 overflow-y-auto p-4 sm:p-6"
|
|
135
|
+
>
|
|
136
|
+
{#if messages.length === 0}
|
|
137
|
+
<div class="flex h-full min-h-[300px] flex-col items-center justify-center text-center">
|
|
138
|
+
<div
|
|
139
|
+
class="mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-primary-50 text-primary-600 dark:bg-primary-950/60 dark:text-primary-400"
|
|
140
|
+
>
|
|
141
|
+
<Icon name="sparkles" size="lg" />
|
|
142
|
+
</div>
|
|
143
|
+
<h3 class="text-base font-semibold text-neutral-900 dark:text-white">
|
|
144
|
+
How can I help you today?
|
|
145
|
+
</h3>
|
|
146
|
+
<p class="mt-1 max-w-sm text-xs text-neutral-500">
|
|
147
|
+
Ask questions, analyze documents, generate code, or explore ideas with interactive AI.
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
{:else}
|
|
151
|
+
{#each messages as msg (msg.id)}
|
|
152
|
+
{#if messageSlot}
|
|
153
|
+
{@render messageSlot(msg)}
|
|
154
|
+
{:else}
|
|
155
|
+
<div class="flex gap-3.5 {msg.role === 'user' ? 'justify-end' : 'justify-start'}">
|
|
156
|
+
{#if msg.role === 'assistant'}
|
|
157
|
+
<Avatar
|
|
158
|
+
src={assistantAvatar}
|
|
159
|
+
alt={assistantName}
|
|
160
|
+
size="sm"
|
|
161
|
+
class="mt-1 flex-shrink-0"
|
|
162
|
+
/>
|
|
163
|
+
{/if}
|
|
164
|
+
|
|
165
|
+
<div
|
|
166
|
+
class="flex max-w-[85%] flex-col gap-1 sm:max-w-[75%] {msg.role === 'user'
|
|
167
|
+
? 'items-end'
|
|
168
|
+
: 'items-start'}"
|
|
169
|
+
>
|
|
170
|
+
<!-- Agent Reasoning / Thoughts -->
|
|
171
|
+
{#if msg.thought}
|
|
172
|
+
<AiThought
|
|
173
|
+
duration={msg.thought.duration}
|
|
174
|
+
running={msg.thought.running}
|
|
175
|
+
steps={msg.thought.steps}
|
|
176
|
+
class="w-full"
|
|
177
|
+
>
|
|
178
|
+
{#if msg.thought.text}
|
|
179
|
+
<p>{msg.thought.text}</p>
|
|
180
|
+
{/if}
|
|
181
|
+
</AiThought>
|
|
182
|
+
{/if}
|
|
183
|
+
|
|
184
|
+
<!-- Agent Tool Invocations -->
|
|
185
|
+
{#if msg.tools && msg.tools.length > 0}
|
|
186
|
+
<div class="w-full space-y-2">
|
|
187
|
+
{#each msg.tools as tool}
|
|
188
|
+
<AiToolCall
|
|
189
|
+
name={tool.name}
|
|
190
|
+
status={tool.status}
|
|
191
|
+
args={tool.args}
|
|
192
|
+
result={tool.result}
|
|
193
|
+
/>
|
|
194
|
+
{/each}
|
|
195
|
+
</div>
|
|
196
|
+
{/if}
|
|
197
|
+
|
|
198
|
+
<!-- Message Bubble -->
|
|
199
|
+
<div
|
|
200
|
+
class="relative rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-xs {msg.role ===
|
|
201
|
+
'user'
|
|
202
|
+
? 'bg-primary-500 text-white dark:bg-primary-600'
|
|
203
|
+
: 'border border-neutral-200/80 bg-neutral-50 text-neutral-900 dark:border-neutral-800/80 dark:bg-neutral-900 dark:text-neutral-100'}"
|
|
204
|
+
>
|
|
205
|
+
<div class="whitespace-pre-wrap">{msg.content}</div>
|
|
206
|
+
|
|
207
|
+
{#if msg.isStreaming}
|
|
208
|
+
<span class="inline-block h-4 w-1.5 animate-pulse bg-current"></span>
|
|
209
|
+
{/if}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<!-- Action Buttons -->
|
|
213
|
+
<div class="mt-1 flex items-center gap-1.5 px-1 text-[11px] text-neutral-400">
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
onclick={() => copyMessage(msg.content)}
|
|
217
|
+
class="hover:text-neutral-700 dark:hover:text-neutral-200"
|
|
218
|
+
title="Copy message"
|
|
219
|
+
>
|
|
220
|
+
<Icon name="copy" size="xs" />
|
|
221
|
+
</button>
|
|
222
|
+
|
|
223
|
+
{#if msg.role === 'assistant' && onretry}
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onclick={() => onretry(msg)}
|
|
227
|
+
class="hover:text-neutral-700 dark:hover:text-neutral-200"
|
|
228
|
+
title="Regenerate response"
|
|
229
|
+
>
|
|
230
|
+
<Icon name="rotate-cw" size="xs" />
|
|
231
|
+
</button>
|
|
232
|
+
{/if}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{#if msg.role === 'user'}
|
|
237
|
+
<Avatar src={userAvatar} alt="User" size="sm" class="mt-1 flex-shrink-0" />
|
|
238
|
+
{/if}
|
|
239
|
+
</div>
|
|
240
|
+
{/if}
|
|
241
|
+
{/each}
|
|
242
|
+
{/if}
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Bottom Prompt Input Bar -->
|
|
246
|
+
<div
|
|
247
|
+
class="border-t border-neutral-200/80 bg-neutral-50/50 p-3 sm:p-4 dark:border-neutral-800/80 dark:bg-neutral-900/50"
|
|
248
|
+
>
|
|
249
|
+
{#if footer}
|
|
250
|
+
{@render footer()}
|
|
251
|
+
{:else}
|
|
252
|
+
<PromptBar
|
|
253
|
+
bind:value={promptValue}
|
|
254
|
+
{placeholder}
|
|
255
|
+
{loading}
|
|
256
|
+
onsubmit={(text, files) => {
|
|
257
|
+
if (onsend) onsend(text, files);
|
|
258
|
+
}}
|
|
259
|
+
{onstop}
|
|
260
|
+
/>
|
|
261
|
+
{/if}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export interface AiMessageThought {
|
|
3
|
+
duration?: number;
|
|
4
|
+
running?: boolean;
|
|
5
|
+
text?: string;
|
|
6
|
+
steps?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface AiMessageTool {
|
|
9
|
+
name: string;
|
|
10
|
+
status?: 'idle' | 'running' | 'success' | 'error';
|
|
11
|
+
args?: Record<string, any> | string;
|
|
12
|
+
result?: Record<string, any> | string;
|
|
13
|
+
}
|
|
14
|
+
export interface AiMessage {
|
|
15
|
+
id: string;
|
|
16
|
+
role: 'user' | 'assistant' | 'system';
|
|
17
|
+
content: string;
|
|
18
|
+
createdAt?: Date | string;
|
|
19
|
+
thought?: AiMessageThought;
|
|
20
|
+
tools?: AiMessageTool[];
|
|
21
|
+
isStreaming?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface AiChatProps {
|
|
24
|
+
messages?: AiMessage[];
|
|
25
|
+
loading?: boolean;
|
|
26
|
+
userAvatar?: string;
|
|
27
|
+
assistantAvatar?: string;
|
|
28
|
+
assistantName?: string;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
autoScroll?: boolean;
|
|
31
|
+
class?: string;
|
|
32
|
+
onsend?: (text: string, files?: File[]) => void;
|
|
33
|
+
onstop?: () => void;
|
|
34
|
+
onretry?: (message: AiMessage) => void;
|
|
35
|
+
header?: Snippet;
|
|
36
|
+
messageSlot?: Snippet<[AiMessage]>;
|
|
37
|
+
footer?: Snippet;
|
|
38
|
+
}
|
|
39
|
+
declare const AiChat: import("svelte").Component<AiChatProps, {}, "">;
|
|
40
|
+
type AiChat = ReturnType<typeof AiChat>;
|
|
41
|
+
export default AiChat;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export interface AiThoughtProps {
|
|
3
|
+
duration?: number;
|
|
4
|
+
running?: boolean;
|
|
5
|
+
title?: string;
|
|
6
|
+
steps?: string[];
|
|
7
|
+
open?: boolean;
|
|
8
|
+
class?: string;
|
|
9
|
+
children?: import('svelte').Snippet;
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import Icon from '../elements/Icon.svelte';
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
duration,
|
|
18
|
+
running = false,
|
|
19
|
+
title = 'Thinking Process',
|
|
20
|
+
steps = [],
|
|
21
|
+
open = $bindable(false),
|
|
22
|
+
class: className = '',
|
|
23
|
+
children
|
|
24
|
+
}: AiThoughtProps = $props();
|
|
25
|
+
|
|
26
|
+
function toggle() {
|
|
27
|
+
open = !open;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
class="my-2 overflow-hidden rounded-xl border border-neutral-200/80 bg-neutral-50/60 transition-all dark:border-neutral-800/80 dark:bg-neutral-900/40 {className}"
|
|
33
|
+
>
|
|
34
|
+
<!-- Header Bar -->
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onclick={toggle}
|
|
38
|
+
class="flex w-full items-center justify-between px-3.5 py-2.5 text-left text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-100/60 dark:text-neutral-300 dark:hover:bg-neutral-800/40"
|
|
39
|
+
aria-expanded={open}
|
|
40
|
+
>
|
|
41
|
+
<div class="flex items-center gap-2">
|
|
42
|
+
{#if running}
|
|
43
|
+
<span class="relative flex h-2 w-2">
|
|
44
|
+
<span
|
|
45
|
+
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary-400 opacity-75"
|
|
46
|
+
></span>
|
|
47
|
+
<span class="relative inline-flex h-2 w-2 rounded-full bg-primary-500"></span>
|
|
48
|
+
</span>
|
|
49
|
+
{:else}
|
|
50
|
+
<Icon name="sparkles" size="xs" class="text-primary-500" />
|
|
51
|
+
{/if}
|
|
52
|
+
|
|
53
|
+
<span class="font-semibold text-neutral-800 dark:text-neutral-200">
|
|
54
|
+
{title}
|
|
55
|
+
</span>
|
|
56
|
+
|
|
57
|
+
{#if running}
|
|
58
|
+
<span class="animate-pulse text-[11px] text-primary-600 dark:text-primary-400">
|
|
59
|
+
Thinking...
|
|
60
|
+
</span>
|
|
61
|
+
{:else if duration !== undefined}
|
|
62
|
+
<span
|
|
63
|
+
class="py-0.2 rounded bg-neutral-200/60 px-1.5 text-[10px] text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400"
|
|
64
|
+
>
|
|
65
|
+
{duration.toFixed(1)}s
|
|
66
|
+
</span>
|
|
67
|
+
{/if}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="flex items-center gap-2 text-neutral-400">
|
|
71
|
+
{#if steps.length > 0}
|
|
72
|
+
<span class="text-[11px] text-neutral-500">
|
|
73
|
+
{steps.length}
|
|
74
|
+
{steps.length === 1 ? 'step' : 'steps'}
|
|
75
|
+
</span>
|
|
76
|
+
{/if}
|
|
77
|
+
<Icon
|
|
78
|
+
name={open ? 'chevron-up' : 'chevron-down'}
|
|
79
|
+
size="xs"
|
|
80
|
+
class="transition-transform duration-200"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</button>
|
|
84
|
+
|
|
85
|
+
<!-- Collapsible Content -->
|
|
86
|
+
{#if open}
|
|
87
|
+
<div
|
|
88
|
+
class="border-t border-neutral-200/60 px-3.5 py-3 text-xs leading-relaxed text-neutral-600 dark:border-neutral-800/60 dark:text-neutral-300"
|
|
89
|
+
>
|
|
90
|
+
{#if steps.length > 0}
|
|
91
|
+
<ul
|
|
92
|
+
class="mb-2.5 space-y-1.5 border-b border-neutral-200/40 pb-2.5 dark:border-neutral-800/40"
|
|
93
|
+
>
|
|
94
|
+
{#each steps as step, idx (idx)}
|
|
95
|
+
<li class="flex items-center gap-2 text-[11px] text-neutral-500 dark:text-neutral-400">
|
|
96
|
+
<Icon name="check" size="xs" class="text-emerald-500" />
|
|
97
|
+
<span>{step}</span>
|
|
98
|
+
</li>
|
|
99
|
+
{/each}
|
|
100
|
+
</ul>
|
|
101
|
+
{/if}
|
|
102
|
+
|
|
103
|
+
{#if children}
|
|
104
|
+
{@render children()}
|
|
105
|
+
{/if}
|
|
106
|
+
</div>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface AiThoughtProps {
|
|
2
|
+
duration?: number;
|
|
3
|
+
running?: boolean;
|
|
4
|
+
title?: string;
|
|
5
|
+
steps?: string[];
|
|
6
|
+
open?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: import('svelte').Snippet;
|
|
9
|
+
}
|
|
10
|
+
declare const AiThought: import("svelte").Component<AiThoughtProps, {}, "open">;
|
|
11
|
+
type AiThought = ReturnType<typeof AiThought>;
|
|
12
|
+
export default AiThought;
|