astryx-dracula 0.5.0 → 0.6.2
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/BRAND.md +3 -3
- package/README.md +7 -4
- package/USAGE.md +49 -49
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +25 -9
- package/astryx-dracula.variants.d.ts +2 -2
- package/astryx.integration.mjs +1 -1
- package/icons.mjs +21 -17
- package/icons.tsx +69 -62
- package/package.json +125 -120
- package/shared/auth-chrome-config.ts +6 -6
- package/shared/auth-chrome.tsx +143 -144
- package/shared/auth-copy.ts +14 -14
- package/shared/chaptered-doc-config.ts +48 -51
- package/shared/chaptered-doc.tsx +267 -289
- package/shared/chart-hues.ts +25 -10
- package/shared/chart-labels.tsx +31 -30
- package/shared/chart-legend.tsx +55 -56
- package/shared/chart-panel-style.ts +3 -3
- package/shared/data-bar.tsx +97 -102
- package/shared/format-money.ts +1 -1
- package/shared/gallery-image.ts +6 -6
- package/shared/login-demo.ts +1 -1
- package/shared/metric-card.tsx +101 -105
- package/shared/metric-delta.tsx +28 -27
- package/shared/order-desk.tsx +205 -216
- package/shared/revenue-chart.tsx +145 -165
- package/shared/scene-castle.tsx +206 -291
- package/shared/scene-frame.tsx +33 -32
- package/shared/scene-hues.ts +17 -17
- package/shared/scene-tile.tsx +88 -92
- package/shared/settings-data.ts +119 -125
- package/shared/settings-rows.tsx +122 -159
- package/shared/sparkline.tsx +75 -88
- package/shared/sso-icons.tsx +10 -22
- package/shared/stars.tsx +16 -25
- package/templates/ai-chat-landing.template.mjs +5 -4
- package/templates/ai-chat-landing.tsx +473 -460
- package/templates/ai-chat.template.mjs +5 -4
- package/templates/ai-chat.tsx +543 -612
- package/templates/blank.template.mjs +5 -4
- package/templates/blank.tsx +18 -14
- package/templates/centered-hero.template.mjs +5 -4
- package/templates/centered-hero.tsx +55 -68
- package/templates/classic-gallery.template.mjs +5 -4
- package/templates/classic-gallery.tsx +97 -104
- package/templates/contact-form.template.mjs +5 -4
- package/templates/contact-form.tsx +247 -282
- package/templates/dashboard-portfolio.template.mjs +5 -4
- package/templates/dashboard-portfolio.tsx +717 -548
- package/templates/dashboard.template.mjs +5 -4
- package/templates/dashboard.tsx +596 -567
- package/templates/detail-page.template.mjs +5 -4
- package/templates/detail-page.tsx +546 -603
- package/templates/documentation-design.template.mjs +5 -4
- package/templates/documentation-design.tsx +231 -243
- package/templates/documentation-technical.template.mjs +5 -4
- package/templates/documentation-technical.tsx +236 -239
- package/templates/documentation.template.mjs +5 -4
- package/templates/documentation.tsx +267 -279
- package/templates/editor.template.mjs +5 -4
- package/templates/editor.tsx +844 -877
- package/templates/file-explorer.template.mjs +5 -4
- package/templates/file-explorer.tsx +566 -590
- package/templates/form-two-column.template.mjs +5 -4
- package/templates/form-two-column.tsx +195 -210
- package/templates/gallery-hero.template.mjs +5 -4
- package/templates/gallery-hero.tsx +93 -114
- package/templates/ide.template.mjs +5 -4
- package/templates/ide.tsx +407 -453
- package/templates/incident-console.template.mjs +5 -4
- package/templates/incident-console.tsx +593 -575
- package/templates/kanban-board.template.mjs +5 -4
- package/templates/kanban-board.tsx +665 -683
- package/templates/library.template.mjs +5 -4
- package/templates/library.tsx +409 -430
- package/templates/login-card.template.mjs +5 -4
- package/templates/login-card.tsx +63 -72
- package/templates/login-split.template.mjs +5 -4
- package/templates/login-split.tsx +192 -193
- package/templates/login-sso.template.mjs +5 -4
- package/templates/login-sso.tsx +257 -258
- package/templates/login.template.mjs +5 -4
- package/templates/login.tsx +53 -49
- package/templates/messaging-shell.template.mjs +5 -4
- package/templates/messaging-shell.tsx +556 -574
- package/templates/mixed-gallery.template.mjs +5 -4
- package/templates/mixed-gallery.tsx +60 -58
- package/templates/payment-form.template.mjs +5 -4
- package/templates/payment-form.tsx +882 -991
- package/templates/product-detail.template.mjs +5 -4
- package/templates/product-detail.tsx +261 -272
- package/templates/product-gallery.template.mjs +5 -4
- package/templates/product-gallery.tsx +126 -137
- package/templates/product-tour.template.mjs +5 -4
- package/templates/product-tour.tsx +741 -773
- package/templates/settings-dialog.template.mjs +5 -4
- package/templates/settings-dialog.tsx +688 -717
- package/templates/settings-sidebar.template.mjs +5 -4
- package/templates/settings-sidebar.tsx +762 -807
- package/templates/settings.template.mjs +5 -4
- package/templates/settings.tsx +289 -304
- package/templates/shell-nav.template.mjs +5 -4
- package/templates/shell-nav.tsx +212 -219
- package/templates/shell-side-nav.template.mjs +5 -4
- package/templates/shell-side-nav.tsx +135 -157
- package/templates/shell-top-nav.template.mjs +5 -4
- package/templates/shell-top-nav.tsx +182 -203
- package/templates/side-gallery.template.mjs +5 -4
- package/templates/side-gallery.tsx +85 -90
- package/templates/table-grouped.template.mjs +5 -4
- package/templates/table-grouped.tsx +1000 -1053
- package/templates/table-page-chart.template.mjs +5 -4
- package/templates/table-page-chart.tsx +400 -398
- package/templates/table-page-heatmap-status.template.mjs +5 -4
- package/templates/table-page-heatmap-status.tsx +483 -512
- package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
- package/templates/table-page-shoe-store-heatmap.tsx +748 -746
- package/templates/table-page.template.mjs +5 -4
- package/templates/table-page.tsx +396 -405
- package/templates/table.template.mjs +5 -4
- package/templates/table.tsx +92 -93
- package/templates/tech-report.template.mjs +5 -4
- package/templates/tech-report.tsx +1316 -1304
- package/templates/theme-showcase.template.mjs +5 -4
- package/templates/theme-showcase.tsx +871 -951
- package/theme.css +35 -77
- package/theme.css.d.ts +9 -0
- package/tokens.css +373 -380
|
@@ -1,44 +1,45 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
// XLE (canonical structure
|
|
3
|
-
//
|
|
2
|
+
// XLE (canonical structure of the tree below; the CLI had no XLE validator
|
|
3
|
+
// after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
|
|
4
|
+
// human drift check — update it with the file, never alone):
|
|
5
|
+
// L[h=fill mw=720] > LC > V[g=8 a=center] > (V[g=1] > (H[g=2 a=center] > Ic + Tx"Hi, Vlad"[t=body]) + Hd"Where should we start?"[level=1 type=display-2]) + ChC"Ask anything" + (V[g=6] > (TgG"Category" > H[g=1 wrap] > Tg"Writing"*4) + VH"Suggested prompts"[as=h2] + (G[c={min:280} g=3] > (CC[p=4] > V[g=0.5] > Hd"Draft"[level=3] + Tx"Compose"[t=body])*4))
|
|
4
6
|
|
|
5
|
-
import {useRef, useState, type CSSProperties} from 'react';
|
|
6
|
-
|
|
7
|
-
import {Layout, LayoutContent, VStack, HStack} from '@astryxdesign/core/Layout';
|
|
8
|
-
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
9
7
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from
|
|
8
|
+
ChatComposer,
|
|
9
|
+
ChatComposerDrawer,
|
|
10
|
+
ChatComposerInput,
|
|
11
|
+
type ChatComposerInputHandle,
|
|
12
|
+
type ChatComposerToken,
|
|
13
|
+
type ChatComposerTrigger,
|
|
14
|
+
ChatDictationButton,
|
|
15
|
+
useChatDictation,
|
|
16
|
+
} from "@astryxdesign/core/Chat";
|
|
17
|
+
import { ClickableCard } from "@astryxdesign/core/ClickableCard";
|
|
18
|
+
import { DropdownMenu, DropdownMenuItem } from "@astryxdesign/core/DropdownMenu";
|
|
19
|
+
import { Grid } from "@astryxdesign/core/Grid";
|
|
20
|
+
import { Icon } from "@astryxdesign/core/Icon";
|
|
21
|
+
import { HStack, Layout, LayoutContent, VStack } from "@astryxdesign/core/Layout";
|
|
22
|
+
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
23
|
+
import { ToggleButton, ToggleButtonGroup } from "@astryxdesign/core/ToggleButton";
|
|
24
|
+
import { Token } from "@astryxdesign/core/Token";
|
|
19
25
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from
|
|
24
|
-
import {
|
|
25
|
-
import {Token} from '@astryxdesign/core/Token';
|
|
26
|
-
import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
|
|
27
|
-
import {ClickableCard} from '@astryxdesign/core/ClickableCard';
|
|
28
|
-
import {Grid} from '@astryxdesign/core/Grid';
|
|
29
|
-
import {Icon} from '@astryxdesign/core/Icon';
|
|
30
|
-
import {DropdownMenu, DropdownMenuItem} from '@astryxdesign/core/DropdownMenu';
|
|
26
|
+
createStaticSource,
|
|
27
|
+
type SearchableItem,
|
|
28
|
+
TypeaheadItem,
|
|
29
|
+
} from "@astryxdesign/core/Typeahead";
|
|
30
|
+
import { VisuallyHidden } from "@astryxdesign/core/VisuallyHidden";
|
|
31
31
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} from
|
|
32
|
+
AtSign,
|
|
33
|
+
Clock,
|
|
34
|
+
CodeXml,
|
|
35
|
+
Lightbulb,
|
|
36
|
+
Lock,
|
|
37
|
+
Search,
|
|
38
|
+
Settings,
|
|
39
|
+
Sparkles,
|
|
40
|
+
SquarePen,
|
|
41
|
+
} from "lucide-react";
|
|
42
|
+
import { type CSSProperties, useRef, useState } from "react";
|
|
42
43
|
|
|
43
44
|
// Fill the content area so the greeting and composer stay vertically centered.
|
|
44
45
|
// `height`, not `minHeight`. A percentage min-height resolves against the
|
|
@@ -48,502 +49,514 @@ import {
|
|
|
48
49
|
// Viewport units are absolute and need no ancestor, which is why every working
|
|
49
50
|
// instance in this repo uses them (editor.tsx:302, file-explorer.tsx:265,
|
|
50
51
|
// messaging-shell.tsx:67, ai-chat.tsx:64, kanban-board.tsx:704).
|
|
51
|
-
const pageStyle: CSSProperties = {height:
|
|
52
|
+
const pageStyle: CSSProperties = { height: "100dvh" };
|
|
52
53
|
// Five --spacing-4 steps: the box opens at ~80px so the empty composer reads
|
|
53
54
|
// as a writing surface, not a single-line field. No single token is 80px.
|
|
54
|
-
const composerInput: CSSProperties = {minHeight:
|
|
55
|
+
const composerInput: CSSProperties = { minHeight: "calc(var(--spacing-4) * 5)" };
|
|
55
56
|
|
|
56
57
|
// Suggestion cards shown once a category is selected.
|
|
57
58
|
const CATEGORY_SUGGESTIONS: Record<
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
string,
|
|
60
|
+
Array<{ heading: string; body: string; prompt: string }>
|
|
60
61
|
> = {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
62
|
+
writing: [
|
|
63
|
+
{
|
|
64
|
+
heading: "Draft a release announcement",
|
|
65
|
+
body: "Compose a clear, polished note for the night-shift crew",
|
|
66
|
+
prompt: "Help me draft a release announcement",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
heading: "Improve my writing",
|
|
70
|
+
body: "Enhance the clarity, tone, and flow of my text",
|
|
71
|
+
prompt: "Review and improve the following text:",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
heading: "Create a palette proposal",
|
|
75
|
+
body: "Write a proposal with hues, ramps, and contrast proofs",
|
|
76
|
+
prompt: "Help me write a palette proposal for",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
heading: "Summarize a document",
|
|
80
|
+
body: "Condense a long grimoire into key takeaways",
|
|
81
|
+
prompt: "Summarize the following document into key points:",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
coding: [
|
|
85
|
+
{
|
|
86
|
+
heading: "Debug my code",
|
|
87
|
+
body: "Find and fix issues in a moonlit code snippet",
|
|
88
|
+
prompt: "Help me debug the following code:",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
heading: "Write a function",
|
|
92
|
+
body: "Conjure a well-typed function warded against the dark",
|
|
93
|
+
prompt: "Write a function that",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
heading: "Theme my editor",
|
|
97
|
+
body: "Port this palette to a Dracula editor theme",
|
|
98
|
+
prompt: "Convert the following colors to a Dracula theme:",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
heading: "Review my pull request",
|
|
102
|
+
body: "Hunt bugs by moonlight, weigh contrast, bless the rite",
|
|
103
|
+
prompt: "Review this code for bugs and improvements:",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
research: [
|
|
107
|
+
{
|
|
108
|
+
heading: "Compare purple hues",
|
|
109
|
+
body: "Analyze the spectral trade-offs of each shade",
|
|
110
|
+
prompt: "Compare the pros and cons of",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
heading: "Explain a concept",
|
|
114
|
+
body: "Unravel a tangled grimoire into plain night-tongue",
|
|
115
|
+
prompt: "Explain the concept of",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
heading: "Find contrast proofs",
|
|
119
|
+
body: "Research AA pairings for text on dark surfaces",
|
|
120
|
+
prompt: "What are the best practices for",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
heading: "Summarize findings",
|
|
124
|
+
body: "Compile research into a structured overview",
|
|
125
|
+
prompt: "Summarize the key findings on",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
creative: [
|
|
129
|
+
{
|
|
130
|
+
heading: "Brainstorm theme variants",
|
|
131
|
+
body: "Generate nocturnal concepts for a new variant",
|
|
132
|
+
prompt: "Brainstorm ideas for",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
heading: "Write a dark tale",
|
|
136
|
+
body: "Spin a moonlit tale crawling with night creatures",
|
|
137
|
+
prompt: "Write a short story about",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
heading: "Design a concept",
|
|
141
|
+
body: "Conjure nocturnal rites for product or visual haunts",
|
|
142
|
+
prompt: "Help me design a concept for",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
heading: "Name a new shade",
|
|
146
|
+
body: "Craft a memorable name for a color or token",
|
|
147
|
+
prompt: "Create a catchy name for",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
149
150
|
};
|
|
150
151
|
|
|
151
152
|
// Category filters, shared by the toggle group and the composer mode menu.
|
|
152
153
|
const CATEGORIES = [
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
{ key: "writing", label: "Writing", icon: SquarePen },
|
|
155
|
+
{ key: "coding", label: "Coding", icon: CodeXml },
|
|
156
|
+
{ key: "research", label: "Research", icon: Search },
|
|
157
|
+
{ key: "creative", label: "Creative", icon: Lightbulb },
|
|
157
158
|
] as const;
|
|
158
159
|
|
|
159
160
|
// Composer mode menu: categories plus special modes.
|
|
160
161
|
const MODE_OPTIONS = [
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
{ key: "auto", label: "Auto", icon: Sparkles },
|
|
163
|
+
...CATEGORIES,
|
|
164
|
+
{ key: "sensitive", label: "Sensitive", icon: Lock },
|
|
165
|
+
{ key: "deep", label: "Deep Mode", icon: Clock },
|
|
165
166
|
] as const;
|
|
166
167
|
|
|
167
168
|
// Modes that insert a composer token instead of switching the active category.
|
|
168
169
|
const TOKEN_MODES: Record<string, string> = {
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
sensitive: "/sensitive",
|
|
171
|
+
deep: "/deep-mode",
|
|
171
172
|
};
|
|
172
173
|
|
|
173
174
|
// Composer trigger data: @ mentions and / commands.
|
|
174
|
-
const MENTION_ITEMS: SearchableItem<{role: string}>[] = [
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
const MENTION_ITEMS: SearchableItem<{ role: string }>[] = [
|
|
176
|
+
{ id: "vesper", label: "Vesper Lin", auxiliaryData: { role: "Design Systems" } },
|
|
177
|
+
{ id: "alex", label: "Alex Nocturne", auxiliaryData: { role: "Frontend" } },
|
|
178
|
+
{ id: "sam", label: "Sam Sable", auxiliaryData: { role: "Backend" } },
|
|
179
|
+
{ id: "jordan", label: "Jordan Hex", auxiliaryData: { role: "Product" } },
|
|
180
|
+
{ id: "taylor", label: "Taylor Twilight", auxiliaryData: { role: "Design" } },
|
|
181
|
+
{ id: "morgan", label: "Morgan Morrow", auxiliaryData: { role: "Infrastructure" } },
|
|
181
182
|
];
|
|
182
183
|
|
|
183
|
-
const COMMAND_ITEMS: SearchableItem<{description: string}>[] = [
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
184
|
+
const COMMAND_ITEMS: SearchableItem<{ description: string }>[] = [
|
|
185
|
+
{
|
|
186
|
+
id: "summarize",
|
|
187
|
+
label: "summarize",
|
|
188
|
+
auxiliaryData: { description: "Summarize the conversation" },
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "translate",
|
|
192
|
+
label: "translate",
|
|
193
|
+
auxiliaryData: { description: "Translate text to another language" },
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "search",
|
|
197
|
+
label: "search",
|
|
198
|
+
auxiliaryData: { description: "Search the web or documents" },
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "code",
|
|
202
|
+
label: "code",
|
|
203
|
+
auxiliaryData: { description: "Generate or explain code" },
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "help",
|
|
207
|
+
label: "help",
|
|
208
|
+
auxiliaryData: { description: "Show available commands" },
|
|
209
|
+
},
|
|
209
210
|
];
|
|
210
211
|
|
|
211
212
|
// One factory for both composer triggers. Render (TypeaheadItem + auxiliary
|
|
212
213
|
// description) and select (token value/label/variant) differ per trigger;
|
|
213
214
|
// the trigger shape doesn't.
|
|
214
215
|
function makeTrigger<TAux>({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
character,
|
|
217
|
+
items,
|
|
218
|
+
getDescription,
|
|
219
|
+
toToken,
|
|
220
|
+
hasMultiWordQuery,
|
|
221
|
+
emptySearchText,
|
|
219
222
|
}: {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
character: string;
|
|
224
|
+
items: SearchableItem<TAux>[];
|
|
225
|
+
getDescription: (aux: TAux | undefined) => string | undefined;
|
|
226
|
+
toToken: (item: SearchableItem<TAux>) => string | ChatComposerToken;
|
|
227
|
+
/** Continue the query past spaces, for names that contain them. */
|
|
228
|
+
hasMultiWordQuery?: boolean;
|
|
229
|
+
/** Shown when the query matched nothing. */
|
|
230
|
+
emptySearchText?: string;
|
|
224
231
|
}): ChatComposerTrigger {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
return {
|
|
233
|
+
character,
|
|
234
|
+
searchSource: createStaticSource(items),
|
|
235
|
+
...(hasMultiWordQuery ? { hasMultiWordQuery } : {}),
|
|
236
|
+
...(emptySearchText ? { emptySearchText } : {}),
|
|
237
|
+
renderItem: (item) => (
|
|
238
|
+
<TypeaheadItem
|
|
239
|
+
item={item}
|
|
240
|
+
description={getDescription(item.auxiliaryData as TAux | undefined)}
|
|
241
|
+
/>
|
|
242
|
+
),
|
|
243
|
+
onSelect: (item) => toToken(item as SearchableItem<TAux>),
|
|
244
|
+
};
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
const mentionTrigger = makeTrigger({
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
248
|
+
character: "@",
|
|
249
|
+
items: MENTION_ITEMS,
|
|
250
|
+
getDescription: (aux) => aux?.role,
|
|
251
|
+
// The mention labels are full names ("Vesper Lin", "Alex Nocturne"), so
|
|
252
|
+
// the query has to survive the space: without this the menu closes at the
|
|
253
|
+
// first space and "@Vesper L" narrows to "Vesper" and then matches
|
|
254
|
+
// nothing. Core hides the menu on an unmatched multi-word query unless
|
|
255
|
+
// the trigger sets emptySearchText, so both go together.
|
|
256
|
+
hasMultiWordQuery: true,
|
|
257
|
+
emptySearchText: "No one by that name tonight.",
|
|
258
|
+
toToken: (item) => ({
|
|
259
|
+
value: `@${item.id}`,
|
|
260
|
+
label: item.label,
|
|
261
|
+
variant: "cyan",
|
|
262
|
+
}),
|
|
247
263
|
});
|
|
248
264
|
|
|
249
265
|
const commandTrigger = makeTrigger({
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
266
|
+
character: "/",
|
|
267
|
+
items: COMMAND_ITEMS,
|
|
268
|
+
getDescription: (aux) => aux?.description,
|
|
269
|
+
toToken: (item) => ({
|
|
270
|
+
value: `/${item.label}`,
|
|
271
|
+
label: `/${item.label}`,
|
|
272
|
+
variant: "yellow",
|
|
273
|
+
}),
|
|
258
274
|
});
|
|
259
275
|
|
|
260
276
|
const composerTriggers = [mentionTrigger, commandTrigger];
|
|
277
|
+
// The upstream `rename emptySearchResultsText to emptySearchText` codemod
|
|
278
|
+
// leaves a TODO on any ChatComposerInput whose `triggers` value it cannot
|
|
279
|
+
// attribute to a literal object — these are built by `makeTrigger`, so it
|
|
280
|
+
// cannot see inside them. The old key appears nowhere in this file, so that
|
|
281
|
+
// TODO would name a rename this template does not need.
|
|
261
282
|
|
|
262
283
|
/** Composer attachment. The name is user-facing; the id is the React key, since
|
|
263
284
|
* two dropped files can share a name. */
|
|
264
285
|
interface Attachment {
|
|
265
|
-
|
|
266
|
-
|
|
286
|
+
id: string;
|
|
287
|
+
name: string;
|
|
267
288
|
}
|
|
268
289
|
|
|
269
290
|
const INITIAL_ATTACHMENTS: Attachment[] = [
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
].map(name => ({id: crypto.randomUUID(), name}));
|
|
291
|
+
"palette_brief.pdf",
|
|
292
|
+
"nocturne_v2.fig",
|
|
293
|
+
"api_spec.yaml",
|
|
294
|
+
"contrast_audit.csv",
|
|
295
|
+
"dracula_spec.pdf",
|
|
296
|
+
].map((name) => ({ id: crypto.randomUUID(), name }));
|
|
276
297
|
|
|
277
298
|
// The composer's imperative insert methods mutate the DOM without emitting a
|
|
278
299
|
// change, so dispatch an input event to sync its value and clear the placeholder.
|
|
279
300
|
const syncComposerValue = () => {
|
|
280
|
-
|
|
301
|
+
document.activeElement?.dispatchEvent(new Event("input", { bubbles: true }));
|
|
281
302
|
};
|
|
282
303
|
|
|
283
304
|
// One inserter for every composer write. `insertText` replaces the selection
|
|
284
305
|
// (suggestion prompts); `insertToken` inserts a chip at the cursor without
|
|
285
306
|
// touching the selection (mentions, mode tokens).
|
|
286
307
|
function insertIntoComposer(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
308
|
+
input: ChatComposerInputHandle | null,
|
|
309
|
+
write: { text: string } | { token: ChatComposerToken },
|
|
310
|
+
collapseToEnd = false,
|
|
290
311
|
) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
312
|
+
if (!input) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
input.focus();
|
|
316
|
+
if (document.activeElement) {
|
|
317
|
+
if (collapseToEnd) {
|
|
318
|
+
const sel = window.getSelection();
|
|
319
|
+
sel?.selectAllChildren(document.activeElement);
|
|
320
|
+
sel?.collapseToEnd();
|
|
321
|
+
} else {
|
|
322
|
+
window.getSelection()?.selectAllChildren(document.activeElement);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if ("text" in write) {
|
|
326
|
+
input.insertText(write.text);
|
|
327
|
+
} else {
|
|
328
|
+
input.insertToken(write.token);
|
|
329
|
+
}
|
|
330
|
+
syncComposerValue();
|
|
310
331
|
}
|
|
311
332
|
|
|
312
333
|
// Main component
|
|
313
334
|
|
|
314
335
|
export default function AiChatLanding() {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// Dictation mic stays: the composer still renders ChatDictationButton below.
|
|
324
|
-
const dictation = useChatDictation({inputRef: composerInputRef});
|
|
336
|
+
const [mode, setMode] = useState<string | null>("auto");
|
|
337
|
+
const [category, setCategory] = useState<string | null>(null);
|
|
338
|
+
const [attachments, setAttachments] = useState<Attachment[]>(INITIAL_ATTACHMENTS);
|
|
339
|
+
const [isModeMenuOpen, setIsModeMenuOpen] = useState(false);
|
|
340
|
+
const composerInputRef = useRef<ChatComposerInputHandle>(null);
|
|
341
|
+
const shouldFocusComposerRef = useRef(false);
|
|
342
|
+
// Dictation mic stays: the composer still renders ChatDictationButton below.
|
|
343
|
+
const dictation = useChatDictation({ inputRef: composerInputRef });
|
|
325
344
|
|
|
326
|
-
|
|
327
|
-
|
|
345
|
+
const activeMode = MODE_OPTIONS.find((m) => m.key === mode) ?? MODE_OPTIONS[0];
|
|
346
|
+
const suggestions = category ? CATEGORY_SUGGESTIONS[category] : null;
|
|
328
347
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
348
|
+
const applySuggestion = (prompt: string) => {
|
|
349
|
+
insertIntoComposer(composerInputRef.current, { text: prompt });
|
|
350
|
+
};
|
|
351
|
+
const insertMention = (item: (typeof MENTION_ITEMS)[number]) => {
|
|
352
|
+
insertIntoComposer(
|
|
353
|
+
composerInputRef.current,
|
|
354
|
+
{
|
|
355
|
+
token: {
|
|
356
|
+
value: `@${item.id}`,
|
|
357
|
+
label: item.label,
|
|
358
|
+
variant: "cyan",
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
true,
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
const insertModeToken = (label: string) => {
|
|
365
|
+
insertIntoComposer(composerInputRef.current, {
|
|
366
|
+
token: {
|
|
367
|
+
value: label,
|
|
368
|
+
label,
|
|
369
|
+
variant: "orange",
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
};
|
|
354
373
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
</Heading>
|
|
374
|
-
</VStack>
|
|
374
|
+
return (
|
|
375
|
+
<Layout
|
|
376
|
+
height="fill"
|
|
377
|
+
contentWidth={720}
|
|
378
|
+
padding={6}
|
|
379
|
+
content={
|
|
380
|
+
<LayoutContent>
|
|
381
|
+
<VStack gap={8} vAlign="center" style={pageStyle}>
|
|
382
|
+
{/* Greeting */}
|
|
383
|
+
<VStack gap={1}>
|
|
384
|
+
<HStack gap={2} vAlign="center">
|
|
385
|
+
<Icon icon={Sparkles} size="md" color="secondary" />
|
|
386
|
+
<Text type="body">Hi, Vlad</Text>
|
|
387
|
+
</HStack>
|
|
388
|
+
<Heading level={1} type="display-2">
|
|
389
|
+
Where should we start?
|
|
390
|
+
</Heading>
|
|
391
|
+
</VStack>
|
|
375
392
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
/>
|
|
393
|
+
{/* Composer */}
|
|
394
|
+
<ChatComposer
|
|
395
|
+
onSubmit={() => {}}
|
|
396
|
+
placeholder="Ask anything"
|
|
397
|
+
input={
|
|
398
|
+
<ChatComposerInput
|
|
399
|
+
handleRef={composerInputRef}
|
|
400
|
+
triggers={composerTriggers}
|
|
401
|
+
style={composerInput}
|
|
402
|
+
onFiles={(files) =>
|
|
403
|
+
setAttachments((prev) => [
|
|
404
|
+
...prev,
|
|
405
|
+
...files.map((file) => ({
|
|
406
|
+
id: crypto.randomUUID(),
|
|
407
|
+
name: file.name,
|
|
408
|
+
})),
|
|
409
|
+
])
|
|
410
|
+
}
|
|
411
|
+
/>
|
|
412
|
+
}
|
|
413
|
+
drawer={
|
|
414
|
+
attachments.length > 0 ? (
|
|
415
|
+
<ChatComposerDrawer count={attachments.length}>
|
|
416
|
+
{attachments.map((attachment) => (
|
|
417
|
+
<Token
|
|
418
|
+
key={attachment.id}
|
|
419
|
+
label={attachment.name}
|
|
420
|
+
onRemove={() =>
|
|
421
|
+
setAttachments((prev) => prev.filter((item) => item.id !== attachment.id))
|
|
422
|
+
}
|
|
423
|
+
/>
|
|
424
|
+
))}
|
|
425
|
+
</ChatComposerDrawer>
|
|
426
|
+
) : undefined
|
|
427
|
+
}
|
|
428
|
+
headerActions={
|
|
429
|
+
<DropdownMenu
|
|
430
|
+
button={{
|
|
431
|
+
label: "Reference",
|
|
432
|
+
variant: "ghost",
|
|
433
|
+
size: "sm",
|
|
434
|
+
icon: <Icon icon={AtSign} size="sm" />,
|
|
435
|
+
isIconOnly: true,
|
|
436
|
+
}}
|
|
437
|
+
hasChevron={false}
|
|
438
|
+
menuWidth={240}
|
|
439
|
+
>
|
|
440
|
+
{MENTION_ITEMS.map((item) => (
|
|
441
|
+
<DropdownMenuItem
|
|
442
|
+
key={item.id}
|
|
443
|
+
label={item.label}
|
|
444
|
+
description={item.auxiliaryData?.role}
|
|
445
|
+
onClick={() => insertMention(item)}
|
|
446
|
+
/>
|
|
447
|
+
))}
|
|
448
|
+
</DropdownMenu>
|
|
449
|
+
}
|
|
450
|
+
footerActions={
|
|
451
|
+
<>
|
|
452
|
+
<DropdownMenu
|
|
453
|
+
button={{
|
|
454
|
+
label: activeMode.label,
|
|
455
|
+
variant: "ghost",
|
|
456
|
+
size: "md",
|
|
457
|
+
icon: <Icon icon={activeMode.icon} size="sm" />,
|
|
458
|
+
children: activeMode.label,
|
|
459
|
+
}}
|
|
460
|
+
menuWidth={200}
|
|
461
|
+
isMenuOpen={isModeMenuOpen}
|
|
462
|
+
onOpenChange={(isOpen) => {
|
|
463
|
+
setIsModeMenuOpen(isOpen);
|
|
464
|
+
// Restore focus to the composer after inserting a mode token.
|
|
465
|
+
if (!isOpen && shouldFocusComposerRef.current) {
|
|
466
|
+
shouldFocusComposerRef.current = false;
|
|
467
|
+
setTimeout(() => composerInputRef.current?.focus(), 50);
|
|
468
|
+
}
|
|
469
|
+
}}
|
|
470
|
+
items={MODE_OPTIONS.flatMap((opt) => {
|
|
471
|
+
const item = {
|
|
472
|
+
label: opt.label,
|
|
473
|
+
icon: opt.icon,
|
|
474
|
+
onClick: () => {
|
|
475
|
+
const tokenLabel = TOKEN_MODES[opt.key];
|
|
476
|
+
if (tokenLabel) {
|
|
477
|
+
insertModeToken(tokenLabel);
|
|
478
|
+
shouldFocusComposerRef.current = true;
|
|
479
|
+
} else {
|
|
480
|
+
setMode(opt.key);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
return opt.key === "sensitive"
|
|
485
|
+
? [{ type: "divider" as const }, item]
|
|
486
|
+
: [item];
|
|
487
|
+
})}
|
|
488
|
+
/>
|
|
489
|
+
<DropdownMenu
|
|
490
|
+
button={{
|
|
491
|
+
label: "Settings",
|
|
492
|
+
variant: "ghost",
|
|
493
|
+
size: "md",
|
|
494
|
+
icon: <Icon icon={Settings} size="sm" />,
|
|
495
|
+
children: "Settings",
|
|
496
|
+
}}
|
|
497
|
+
menuWidth={200}
|
|
498
|
+
items={[
|
|
499
|
+
{ label: "Preferences", onClick: () => {} },
|
|
500
|
+
{ label: "Keyboard shortcuts", onClick: () => {} },
|
|
501
|
+
{ label: "About", onClick: () => {} },
|
|
502
|
+
]}
|
|
503
|
+
/>
|
|
504
|
+
</>
|
|
505
|
+
}
|
|
506
|
+
sendActions={<ChatDictationButton dictation={dictation} />}
|
|
507
|
+
/>
|
|
492
508
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
value={category}
|
|
498
|
-
onChange={setCategory}
|
|
499
|
-
size="lg">
|
|
500
|
-
{/* ToggleButtonGroup is a single nowrap flex row and the four
|
|
509
|
+
{/* Category filters + suggestion cards */}
|
|
510
|
+
<VStack gap={6}>
|
|
511
|
+
<ToggleButtonGroup label="Category" value={category} onChange={setCategory} size="lg">
|
|
512
|
+
{/* ToggleButtonGroup is a single nowrap flex row and the four
|
|
501
513
|
chips measure 488px together. In a 390px pane the fourth
|
|
502
514
|
chip was cut in half by the viewport edge, which reads as a
|
|
503
515
|
rendering fault rather than as overflow. The wrapping
|
|
504
516
|
HStack gives the row a second line instead; the group keeps
|
|
505
517
|
its own selection semantics either way. */}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
518
|
+
<HStack gap={1} wrap="wrap">
|
|
519
|
+
{CATEGORIES.map((cat) => (
|
|
520
|
+
<ToggleButton
|
|
521
|
+
key={cat.key}
|
|
522
|
+
value={cat.key}
|
|
523
|
+
label={cat.label}
|
|
524
|
+
icon={<Icon icon={cat.icon} size="sm" />}
|
|
525
|
+
/>
|
|
526
|
+
))}
|
|
527
|
+
</HStack>
|
|
528
|
+
</ToggleButtonGroup>
|
|
517
529
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
530
|
+
{suggestions && (
|
|
531
|
+
<>
|
|
532
|
+
<VisuallyHidden as="h2">Suggested prompts</VisuallyHidden>
|
|
533
|
+
<Grid columns={{ minWidth: 280 }} gap={3}>
|
|
534
|
+
{suggestions.map((suggestion) => (
|
|
535
|
+
<ClickableCard
|
|
536
|
+
key={suggestion.heading}
|
|
537
|
+
label={suggestion.heading}
|
|
538
|
+
variant="muted"
|
|
539
|
+
padding={4}
|
|
540
|
+
onClick={() => {
|
|
541
|
+
applySuggestion(suggestion.prompt);
|
|
542
|
+
setMode(category);
|
|
543
|
+
}}
|
|
544
|
+
>
|
|
545
|
+
<VStack gap={0.5}>
|
|
546
|
+
<Heading level={3}>{suggestion.heading}</Heading>
|
|
547
|
+
<Text type="body" color="secondary">
|
|
548
|
+
{suggestion.body}
|
|
549
|
+
</Text>
|
|
550
|
+
</VStack>
|
|
551
|
+
</ClickableCard>
|
|
552
|
+
))}
|
|
553
|
+
</Grid>
|
|
554
|
+
</>
|
|
555
|
+
)}
|
|
556
|
+
</VStack>
|
|
557
|
+
</VStack>
|
|
558
|
+
</LayoutContent>
|
|
559
|
+
}
|
|
560
|
+
/>
|
|
561
|
+
);
|
|
549
562
|
}
|