From 59076346f4eb8131cd26f00272a958809f91fef3 Mon Sep 17 00:00:00 2001 From: Jorge Calvar Date: Wed, 18 Feb 2026 17:35:47 +0100 Subject: [PATCH] feat(appkit-ui): add Genie chat React components and dev-playground demo Add plug-and-play React components for Genie AI/BI chat: - useGenieChat hook: manages SSE streaming, conversation persistence via URL params, and history replay on page refresh - GenieChat: all-in-one component wiring hook + UI - GenieChatMessage: renders messages with markdown (via marked), avatars, and collapsible SQL query attachments - GenieChatMessageList: scrollable message list with auto-scroll, loading skeletons, and streaming status indicator - GenieChatInput: textarea with Enter-to-send and auto-resize Also adds Genie demo page to dev-playground at /genie and fixes conversation history ordering in the backend (reverse to chronological). Signed-off-by: Jorge Calvar --- apps/dev-playground/.env.dist | 1 + .../client/src/routeTree.gen.ts | 21 + .../client/src/routes/__root.tsx | 8 + .../client/src/routes/genie.route.tsx | 29 + .../client/src/routes/index.tsx | 19 + apps/dev-playground/server/index.ts | 5 +- docs/static/appkit-ui/styles.gen.css | 784 ++++++++++++++++++ packages/appkit-ui/package.json | 1 + .../src/react/genie/genie-chat-input.tsx | 74 ++ .../react/genie/genie-chat-message-list.tsx | 84 ++ .../src/react/genie/genie-chat-message.tsx | 120 +++ .../appkit-ui/src/react/genie/genie-chat.tsx | 49 ++ packages/appkit-ui/src/react/genie/index.ts | 6 + packages/appkit-ui/src/react/genie/types.ts | 90 ++ .../src/react/genie/use-genie-chat.ts | 313 +++++++ packages/appkit-ui/src/react/index.ts | 1 + packages/appkit/src/plugins/genie/genie.ts | 3 +- pnpm-lock.yaml | 144 ++-- 18 files changed, 1683 insertions(+), 69 deletions(-) create mode 100644 apps/dev-playground/client/src/routes/genie.route.tsx create mode 100644 packages/appkit-ui/src/react/genie/genie-chat-input.tsx create mode 100644 packages/appkit-ui/src/react/genie/genie-chat-message-list.tsx create mode 100644 packages/appkit-ui/src/react/genie/genie-chat-message.tsx create mode 100644 packages/appkit-ui/src/react/genie/genie-chat.tsx create mode 100644 packages/appkit-ui/src/react/genie/index.ts create mode 100644 packages/appkit-ui/src/react/genie/types.ts create mode 100644 packages/appkit-ui/src/react/genie/use-genie-chat.ts diff --git a/apps/dev-playground/.env.dist b/apps/dev-playground/.env.dist index 7a7076ff..e17b89d5 100644 --- a/apps/dev-playground/.env.dist +++ b/apps/dev-playground/.env.dist @@ -6,3 +6,4 @@ NODE_ENV='development' OTEL_EXPORTER_OTLP_ENDPOINT='http://localhost:4318' OTEL_RESOURCE_ATTRIBUTES='service.sample_attribute=dev' OTEL_SERVICE_NAME='dev-playground' +GENIE_SPACE_ID= diff --git a/apps/dev-playground/client/src/routeTree.gen.ts b/apps/dev-playground/client/src/routeTree.gen.ts index f9b31113..904e257b 100644 --- a/apps/dev-playground/client/src/routeTree.gen.ts +++ b/apps/dev-playground/client/src/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as TypeSafetyRouteRouteImport } from './routes/type-safety.route' import { Route as TelemetryRouteRouteImport } from './routes/telemetry.route' import { Route as SqlHelpersRouteRouteImport } from './routes/sql-helpers.route' import { Route as ReconnectRouteRouteImport } from './routes/reconnect.route' +import { Route as GenieRouteRouteImport } from './routes/genie.route' import { Route as DataVisualizationRouteRouteImport } from './routes/data-visualization.route' import { Route as ArrowAnalyticsRouteRouteImport } from './routes/arrow-analytics.route' import { Route as AnalyticsRouteRouteImport } from './routes/analytics.route' @@ -38,6 +39,11 @@ const ReconnectRouteRoute = ReconnectRouteRouteImport.update({ path: '/reconnect', getParentRoute: () => rootRouteImport, } as any) +const GenieRouteRoute = GenieRouteRouteImport.update({ + id: '/genie', + path: '/genie', + getParentRoute: () => rootRouteImport, +} as any) const DataVisualizationRouteRoute = DataVisualizationRouteRouteImport.update({ id: '/data-visualization', path: '/data-visualization', @@ -64,6 +70,7 @@ export interface FileRoutesByFullPath { '/analytics': typeof AnalyticsRouteRoute '/arrow-analytics': typeof ArrowAnalyticsRouteRoute '/data-visualization': typeof DataVisualizationRouteRoute + '/genie': typeof GenieRouteRoute '/reconnect': typeof ReconnectRouteRoute '/sql-helpers': typeof SqlHelpersRouteRoute '/telemetry': typeof TelemetryRouteRoute @@ -74,6 +81,7 @@ export interface FileRoutesByTo { '/analytics': typeof AnalyticsRouteRoute '/arrow-analytics': typeof ArrowAnalyticsRouteRoute '/data-visualization': typeof DataVisualizationRouteRoute + '/genie': typeof GenieRouteRoute '/reconnect': typeof ReconnectRouteRoute '/sql-helpers': typeof SqlHelpersRouteRoute '/telemetry': typeof TelemetryRouteRoute @@ -85,6 +93,7 @@ export interface FileRoutesById { '/analytics': typeof AnalyticsRouteRoute '/arrow-analytics': typeof ArrowAnalyticsRouteRoute '/data-visualization': typeof DataVisualizationRouteRoute + '/genie': typeof GenieRouteRoute '/reconnect': typeof ReconnectRouteRoute '/sql-helpers': typeof SqlHelpersRouteRoute '/telemetry': typeof TelemetryRouteRoute @@ -97,6 +106,7 @@ export interface FileRouteTypes { | '/analytics' | '/arrow-analytics' | '/data-visualization' + | '/genie' | '/reconnect' | '/sql-helpers' | '/telemetry' @@ -107,6 +117,7 @@ export interface FileRouteTypes { | '/analytics' | '/arrow-analytics' | '/data-visualization' + | '/genie' | '/reconnect' | '/sql-helpers' | '/telemetry' @@ -117,6 +128,7 @@ export interface FileRouteTypes { | '/analytics' | '/arrow-analytics' | '/data-visualization' + | '/genie' | '/reconnect' | '/sql-helpers' | '/telemetry' @@ -128,6 +140,7 @@ export interface RootRouteChildren { AnalyticsRouteRoute: typeof AnalyticsRouteRoute ArrowAnalyticsRouteRoute: typeof ArrowAnalyticsRouteRoute DataVisualizationRouteRoute: typeof DataVisualizationRouteRoute + GenieRouteRoute: typeof GenieRouteRoute ReconnectRouteRoute: typeof ReconnectRouteRoute SqlHelpersRouteRoute: typeof SqlHelpersRouteRoute TelemetryRouteRoute: typeof TelemetryRouteRoute @@ -164,6 +177,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ReconnectRouteRouteImport parentRoute: typeof rootRouteImport } + '/genie': { + id: '/genie' + path: '/genie' + fullPath: '/genie' + preLoaderRoute: typeof GenieRouteRouteImport + parentRoute: typeof rootRouteImport + } '/data-visualization': { id: '/data-visualization' path: '/data-visualization' @@ -200,6 +220,7 @@ const rootRouteChildren: RootRouteChildren = { AnalyticsRouteRoute: AnalyticsRouteRoute, ArrowAnalyticsRouteRoute: ArrowAnalyticsRouteRoute, DataVisualizationRouteRoute: DataVisualizationRouteRoute, + GenieRouteRoute: GenieRouteRoute, ReconnectRouteRoute: ReconnectRouteRoute, SqlHelpersRouteRoute: SqlHelpersRouteRoute, TelemetryRouteRoute: TelemetryRouteRoute, diff --git a/apps/dev-playground/client/src/routes/__root.tsx b/apps/dev-playground/client/src/routes/__root.tsx index b2faa651..3ad7ca0c 100644 --- a/apps/dev-playground/client/src/routes/__root.tsx +++ b/apps/dev-playground/client/src/routes/__root.tsx @@ -72,6 +72,14 @@ function RootComponent() { SQL Helpers + + + diff --git a/apps/dev-playground/client/src/routes/genie.route.tsx b/apps/dev-playground/client/src/routes/genie.route.tsx new file mode 100644 index 00000000..3b47ce37 --- /dev/null +++ b/apps/dev-playground/client/src/routes/genie.route.tsx @@ -0,0 +1,29 @@ +import { GenieChat } from "@databricks/appkit-ui/react"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/genie")({ + component: GenieRoute, +}); + +function GenieRoute() { + return ( +
+
+
+
+

+ Genie Chat +

+

+ Ask natural language questions about your data using AI/BI Genie. +

+
+ +
+ +
+
+
+
+ ); +} diff --git a/apps/dev-playground/client/src/routes/index.tsx b/apps/dev-playground/client/src/routes/index.tsx index c6d5b7fc..b6ff92fb 100644 --- a/apps/dev-playground/client/src/routes/index.tsx +++ b/apps/dev-playground/client/src/routes/index.tsx @@ -162,6 +162,25 @@ function IndexRoute() { + + +
+

+ Genie Chat +

+

+ Ask natural language questions about your data using AI/BI + Genie. Features SSE streaming, markdown rendering, and + conversation persistence. +

+ +
+
diff --git a/apps/dev-playground/server/index.ts b/apps/dev-playground/server/index.ts index a56ba4a7..4da9295b 100644 --- a/apps/dev-playground/server/index.ts +++ b/apps/dev-playground/server/index.ts @@ -1,4 +1,4 @@ -import { analytics, createApp, server } from "@databricks/appkit"; +import { analytics, createApp, genie, server } from "@databricks/appkit"; import { WorkspaceClient } from "@databricks/sdk-experimental"; import { reconnect } from "./reconnect-plugin"; import { telemetryExamples } from "./telemetry-example-plugin"; @@ -19,6 +19,9 @@ createApp({ reconnect(), telemetryExamples(), analytics({}), + genie({ + spaces: { demo: process.env.GENIE_SPACE_ID ?? "placeholder" }, + }), ], ...(process.env.APPKIT_E2E_TEST && { client: createMockClient() }), }).then((appkit) => { diff --git a/docs/static/appkit-ui/styles.gen.css b/docs/static/appkit-ui/styles.gen.css index a9f095f5..5a11e82f 100644 --- a/docs/static/appkit-ui/styles.gen.css +++ b/docs/static/appkit-ui/styles.gen.css @@ -8,7 +8,42 @@ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --color-red-50: oklch(97.1% 0.013 17.38); + --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-200: oklch(88.5% 0.062 18.334); + --color-red-700: oklch(50.5% 0.213 27.518); + --color-amber-500: oklch(76.9% 0.188 70.08); + --color-yellow-100: oklch(97.3% 0.071 103.193); + --color-yellow-200: oklch(94.5% 0.129 101.54); + --color-yellow-300: oklch(90.5% 0.182 98.111); + --color-yellow-600: oklch(68.1% 0.162 75.834); + --color-yellow-700: oklch(55.4% 0.135 66.442); + --color-green-50: oklch(98.2% 0.018 155.826); + --color-green-100: oklch(96.2% 0.044 156.743); + --color-green-200: oklch(92.5% 0.084 155.995); + --color-green-400: oklch(79.2% 0.209 151.711); + --color-green-500: oklch(72.3% 0.219 149.579); + --color-green-600: oklch(62.7% 0.194 149.214); + --color-green-700: oklch(52.7% 0.154 150.069); + --color-emerald-100: oklch(95% 0.052 163.051); + --color-emerald-200: oklch(90.5% 0.093 164.15); + --color-emerald-700: oklch(50.8% 0.118 165.612); --color-sky-500: oklch(68.5% 0.169 237.323); + --color-blue-100: oklch(93.2% 0.032 255.585); + --color-blue-200: oklch(88.2% 0.059 254.128); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-700: oklch(48.8% 0.243 264.376); + --color-blue-800: oklch(42.4% 0.199 265.638); + --color-gray-50: oklch(98.5% 0.002 247.839); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-200: oklch(92.8% 0.006 264.531); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-600: oklch(44.6% 0.03 256.802); + --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-800: oklch(27.8% 0.033 256.848); + --color-gray-900: oklch(21% 0.034 264.665); + --color-zinc-900: oklch(21% 0.006 285.885); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; @@ -16,6 +51,10 @@ --container-sm: 24rem; --container-md: 28rem; --container-lg: 32rem; + --container-2xl: 42rem; + --container-4xl: 56rem; + --container-6xl: 72rem; + --container-7xl: 80rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; @@ -24,10 +63,16 @@ --text-base--line-height: calc(1.5 / 1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5 / 2.25); + --text-5xl: 3rem; + --text-5xl--line-height: 1; --text-7xl: 4.5rem; --text-7xl--line-height: 1; --font-weight-normal: 400; @@ -46,6 +91,7 @@ --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem; + --ease-out: cubic-bezier(0, 0, 0.2, 1); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --animate-spin: spin 1s linear infinite; --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; @@ -283,9 +329,15 @@ .top-\[50\%\] { top: 50%; } + .top-\[52px\] { + top: 52px; + } .top-\[60\%\] { top: 60%; } + .top-\[84px\] { + top: 84px; + } .top-full { top: 100%; } @@ -307,12 +359,18 @@ .right-4 { right: calc(var(--spacing) * 4); } + .right-8 { + right: calc(var(--spacing) * 8); + } .-bottom-12 { bottom: calc(var(--spacing) * -12); } .bottom-0 { bottom: calc(var(--spacing) * 0); } + .bottom-\[24px\] { + bottom: 24px; + } .-left-12 { left: calc(var(--spacing) * -12); } @@ -325,12 +383,27 @@ .left-2 { left: calc(var(--spacing) * 2); } + .left-8 { + left: calc(var(--spacing) * 8); + } .left-\[50\%\] { left: 50%; } + .left-\[88px\] { + left: 88px; + } + .left-\[100px\] { + left: 100px; + } + .left-\[220px\] { + left: 220px; + } .isolate { isolation: isolate; } + .z-0 { + z-index: 0; + } .z-10 { z-index: 10; } @@ -358,6 +431,21 @@ .col-start-2 { grid-column-start: 2; } + .row-1 { + grid-row: 1; + } + .row-2 { + grid-row: 2; + } + .row-3 { + grid-row: 3; + } + .row-4 { + grid-row: 4; + } + .row-5 { + grid-row: 5; + } .row-span-2 { grid-row: span 2 / span 2; } @@ -388,12 +476,18 @@ .\!m-0 { margin: calc(var(--spacing) * 0) !important; } + .m-4 { + margin: calc(var(--spacing) * 4); + } .-mx-1 { margin-inline: calc(var(--spacing) * -1); } .mx-2 { margin-inline: calc(var(--spacing) * 2); } + .mx-3 { + margin-inline: calc(var(--spacing) * 3); + } .mx-3\.5 { margin-inline: calc(var(--spacing) * 3.5); } @@ -421,6 +515,9 @@ .-mt-4 { margin-top: calc(var(--spacing) * -4); } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } .mt-1\.5 { margin-top: calc(var(--spacing) * 1.5); } @@ -433,12 +530,18 @@ .mt-4 { margin-top: calc(var(--spacing) * 4); } + .mt-8 { + margin-top: calc(var(--spacing) * 8); + } .mt-auto { margin-top: auto; } .mr-2 { margin-right: calc(var(--spacing) * 2); } + .mb-1 { + margin-bottom: calc(var(--spacing) * 1); + } .mb-2 { margin-bottom: calc(var(--spacing) * 2); } @@ -448,6 +551,15 @@ .mb-4 { margin-bottom: calc(var(--spacing) * 4); } + .mb-6 { + margin-bottom: calc(var(--spacing) * 6); + } + .mb-8 { + margin-bottom: calc(var(--spacing) * 8); + } + .mb-16 { + margin-bottom: calc(var(--spacing) * 16); + } .-ml-4 { margin-left: calc(var(--spacing) * -4); } @@ -460,6 +572,9 @@ .ml-4 { margin-left: calc(var(--spacing) * 4); } + .ml-5 { + margin-left: calc(var(--spacing) * 5); + } .ml-auto { margin-left: auto; } @@ -572,6 +687,9 @@ .h-\(--cell-size\) { height: var(--cell-size); } + .h-0\.5 { + height: calc(var(--spacing) * 0.5); + } .h-1\.5 { height: calc(var(--spacing) * 1.5); } @@ -581,6 +699,9 @@ .h-2\.5 { height: calc(var(--spacing) * 2.5); } + .h-3 { + height: calc(var(--spacing) * 3); + } .h-4 { height: calc(var(--spacing) * 4); } @@ -605,6 +726,9 @@ .h-12 { height: calc(var(--spacing) * 12); } + .h-20 { + height: calc(var(--spacing) * 20); + } .h-24 { height: calc(var(--spacing) * 24); } @@ -623,6 +747,9 @@ .h-\[200px\] { height: 200px; } + .h-\[600px\] { + height: 600px; + } .h-\[calc\(100\%-1px\)\] { height: calc(100% - 1px); } @@ -653,9 +780,15 @@ .max-h-\(--radix-select-content-available-height\) { max-height: var(--radix-select-content-available-height); } + .max-h-96 { + max-height: calc(var(--spacing) * 96); + } .max-h-\[300px\] { max-height: 300px; } + .max-h-\[400px\] { + max-height: 400px; + } .max-h-\[600px\] { max-height: 600px; } @@ -668,6 +801,15 @@ .min-h-16 { min-height: calc(var(--spacing) * 16); } + .min-h-\[300px\] { + min-height: 300px; + } + .min-h-\[400px\] { + min-height: 400px; + } + .min-h-screen { + min-height: 100vh; + } .min-h-svh { min-height: 100svh; } @@ -683,12 +825,18 @@ .w-1 { width: calc(var(--spacing) * 1); } + .w-1\/2 { + width: calc(1/2 * 100%); + } .w-2 { width: calc(var(--spacing) * 2); } .w-2\.5 { width: calc(var(--spacing) * 2.5); } + .w-2\/3 { + width: calc(2/3 * 100%); + } .w-3 { width: calc(var(--spacing) * 3); } @@ -698,6 +846,9 @@ .w-4 { width: calc(var(--spacing) * 4); } + .w-4\/5 { + width: calc(4/5 * 100%); + } .w-5 { width: calc(var(--spacing) * 5); } @@ -707,6 +858,9 @@ .w-9 { width: calc(var(--spacing) * 9); } + .w-10 { + width: calc(var(--spacing) * 10); + } .w-12 { width: calc(var(--spacing) * 12); } @@ -716,6 +870,9 @@ .w-24 { width: calc(var(--spacing) * 24); } + .w-28 { + width: calc(var(--spacing) * 28); + } .w-32 { width: calc(var(--spacing) * 32); } @@ -725,6 +882,9 @@ .w-56 { width: calc(var(--spacing) * 56); } + .w-60 { + width: calc(var(--spacing) * 60); + } .w-64 { width: calc(var(--spacing) * 64); } @@ -746,6 +906,12 @@ .w-\[100px\] { width: 100px; } + .w-\[130px\] { + width: 130px; + } + .w-\[150px\] { + width: 150px; + } .w-\[180px\] { width: 180px; } @@ -785,9 +951,30 @@ .max-w-\(--skeleton-width\) { max-width: var(--skeleton-width); } + .max-w-2xl { + max-width: var(--container-2xl); + } + .max-w-4xl { + max-width: var(--container-4xl); + } + .max-w-6xl { + max-width: var(--container-6xl); + } + .max-w-7xl { + max-width: var(--container-7xl); + } + .max-w-\[80\%\] { + max-width: 80%; + } + .max-w-\[1200px\] { + max-width: 1200px; + } .max-w-\[calc\(100\%-2rem\)\] { max-width: calc(100% - 2rem); } + .max-w-full { + max-width: 100%; + } .max-w-max { max-width: max-content; } @@ -836,6 +1023,9 @@ .shrink-0 { flex-shrink: 0; } + .flex-grow { + flex-grow: 1; + } .grow { flex-grow: 1; } @@ -932,6 +1122,9 @@ .cursor-default { cursor: default; } + .cursor-pointer { + cursor: pointer; + } .cursor-text { cursor: text; } @@ -956,6 +1149,9 @@ .auto-rows-min { grid-auto-rows: min-content; } + .grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -983,6 +1179,9 @@ .flex-row { flex-direction: row; } + .flex-row-reverse { + flex-direction: row-reverse; + } .flex-wrap { flex-wrap: wrap; } @@ -1037,6 +1236,9 @@ .gap-4 { gap: calc(var(--spacing) * 4); } + .gap-5 { + gap: calc(var(--spacing) * 5); + } .gap-6 { gap: calc(var(--spacing) * 6); } @@ -1067,6 +1269,20 @@ margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-4 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-6 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-x-2 { :where(& > :not(:last-child)) { --tw-space-x-reverse: 0; @@ -1084,6 +1300,9 @@ .gap-y-0\.5 { row-gap: calc(var(--spacing) * 0.5); } + .self-end { + align-self: flex-end; + } .self-start { align-self: flex-start; } @@ -1168,6 +1387,14 @@ border-style: var(--tw-border-style); border-width: 0px; } + .border-2 { + border-style: var(--tw-border-style); + border-width: 2px; + } + .border-3 { + border-style: var(--tw-border-style); + border-width: 3px; + } .border-\[1\.5px\] { border-style: var(--tw-border-style); border-width: 1.5px; @@ -1199,6 +1426,12 @@ .border-\(--color-border\) { border-color: var(--color-border); } + .border-\[\#454545\] { + border-color: #454545; + } + .border-blue-200 { + border-color: var(--color-blue-200); + } .border-border { border-color: var(--border); } @@ -1208,18 +1441,54 @@ border-color: color-mix(in oklab, var(--border) 50%, transparent); } } + .border-destructive\/20 { + border-color: var(--destructive); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--destructive) 20%, transparent); + } + } + .border-emerald-200 { + border-color: var(--color-emerald-200); + } + .border-gray-200 { + border-color: var(--color-gray-200); + } + .border-gray-400 { + border-color: var(--color-gray-400); + } + .border-green-200 { + border-color: var(--color-green-200); + } + .border-green-600 { + border-color: var(--color-green-600); + } .border-input { border-color: var(--input); } .border-primary { border-color: var(--primary); } + .border-red-200 { + border-color: var(--color-red-200); + } .border-sidebar-border { border-color: var(--sidebar-border); } + .border-success\/20 { + border-color: var(--success); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--success) 20%, transparent); + } + } .border-transparent { border-color: transparent; } + .border-yellow-200 { + border-color: var(--color-yellow-200); + } + .border-yellow-600 { + border-color: var(--color-yellow-600); + } .border-t-transparent { border-top-color: transparent; } @@ -1229,9 +1498,18 @@ .bg-\(--color-bg\) { background-color: var(--color-bg); } + .bg-\[\#04395e\] { + background-color: #04395e; + } + .bg-\[\#252526\] { + background-color: #252526; + } .bg-accent { background-color: var(--accent); } + .bg-amber-500 { + background-color: var(--color-amber-500); + } .bg-background { background-color: var(--background); } @@ -1241,6 +1519,9 @@ background-color: color-mix(in oklab, var(--color-black) 50%, transparent); } } + .bg-blue-100 { + background-color: var(--color-blue-100); + } .bg-border { background-color: var(--border); } @@ -1250,15 +1531,51 @@ .bg-destructive { background-color: var(--destructive); } + .bg-destructive\/10 { + background-color: var(--destructive); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--destructive) 10%, transparent); + } + } + .bg-emerald-100 { + background-color: var(--color-emerald-100); + } .bg-foreground { background-color: var(--foreground); } + .bg-gray-50 { + background-color: var(--color-gray-50); + } + .bg-gray-100 { + background-color: var(--color-gray-100); + } + .bg-gray-200 { + background-color: var(--color-gray-200); + } + .bg-gray-800 { + background-color: var(--color-gray-800); + } + .bg-green-50 { + background-color: var(--color-green-50); + } + .bg-green-100 { + background-color: var(--color-green-100); + } + .bg-green-400 { + background-color: var(--color-green-400); + } .bg-input { background-color: var(--input); } .bg-muted { background-color: var(--muted); } + .bg-muted-foreground\/40 { + background-color: var(--muted-foreground); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--muted-foreground) 40%, transparent); + } + } .bg-muted\/50 { background-color: var(--muted); @supports (color: color-mix(in lab, red, red)) { @@ -1277,6 +1594,12 @@ background-color: color-mix(in oklab, var(--primary) 20%, transparent); } } + .bg-red-50 { + background-color: var(--color-red-50); + } + .bg-red-100 { + background-color: var(--color-red-100); + } .bg-secondary { background-color: var(--secondary); } @@ -1289,12 +1612,33 @@ .bg-sky-500 { background-color: var(--color-sky-500); } + .bg-success { + background-color: var(--success); + } + .bg-success\/10 { + background-color: var(--success); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--success) 10%, transparent); + } + } .bg-transparent { background-color: transparent; } .bg-white { background-color: var(--color-white); } + .bg-white\/90 { + background-color: color-mix(in srgb, #fff 90%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 90%, transparent); + } + } + .bg-yellow-100 { + background-color: var(--color-yellow-100); + } + .bg-yellow-300 { + background-color: var(--color-yellow-300); + } .bg-gradient-to-b { --tw-gradient-position: to bottom in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); @@ -1337,12 +1681,18 @@ .p-4 { padding: calc(var(--spacing) * 4); } + .p-5 { + padding: calc(var(--spacing) * 5); + } .p-6 { padding: calc(var(--spacing) * 6); } .p-8 { padding: calc(var(--spacing) * 8); } + .p-10 { + padding: calc(var(--spacing) * 10); + } .p-\[3px\] { padding: 3px; } @@ -1373,6 +1723,12 @@ .px-6 { padding-inline: calc(var(--spacing) * 6); } + .px-11 { + padding-inline: calc(var(--spacing) * 11); + } + .px-\[1px\] { + padding-inline: 1px; + } .py-0\.5 { padding-block: calc(var(--spacing) * 0.5); } @@ -1394,6 +1750,12 @@ .py-6 { padding-block: calc(var(--spacing) * 6); } + .py-12 { + padding-block: calc(var(--spacing) * 12); + } + .py-20 { + padding-block: calc(var(--spacing) * 20); + } .pt-0 { padding-top: calc(var(--spacing) * 0); } @@ -1406,6 +1768,12 @@ .pt-4 { padding-top: calc(var(--spacing) * 4); } + .pt-6 { + padding-top: calc(var(--spacing) * 6); + } + .pt-12 { + padding-top: calc(var(--spacing) * 12); + } .pr-1 { padding-right: calc(var(--spacing) * 1); } @@ -1424,6 +1792,9 @@ .pb-0 { padding-bottom: calc(var(--spacing) * 0); } + .pb-2 { + padding-bottom: calc(var(--spacing) * 2); + } .pb-3 { padding-bottom: calc(var(--spacing) * 3); } @@ -1464,10 +1835,18 @@ font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } .text-4xl { font-size: var(--text-4xl); line-height: var(--tw-leading, var(--text-4xl--line-height)); } + .text-5xl { + font-size: var(--text-5xl); + line-height: var(--tw-leading, var(--text-5xl--line-height)); + } .text-7xl { font-size: var(--text-7xl); line-height: var(--tw-leading, var(--text-7xl--line-height)); @@ -1488,6 +1867,10 @@ font-size: var(--text-sm); line-height: var(--leading-relaxed); } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } .text-xs { font-size: var(--text-xs); line-height: var(--tw-leading, var(--text-xs--line-height)); @@ -1498,6 +1881,13 @@ .text-\[0\.70rem\] { font-size: 0.70rem; } + .text-\[11px\] { + font-size: 11px; + } + .leading-7 { + --tw-leading: calc(var(--spacing) * 7); + line-height: calc(var(--spacing) * 7); + } .leading-none { --tw-leading: 1; line-height: 1; @@ -1551,12 +1941,27 @@ .whitespace-nowrap { white-space: nowrap; } + .whitespace-pre-wrap { + white-space: pre-wrap; + } + .text-\[\#858585\] { + color: #858585; + } + .text-\[\#cccccc\] { + color: #cccccc; + } .text-accent-foreground { color: var(--accent-foreground); } .text-background { color: var(--background); } + .text-blue-600 { + color: var(--color-blue-600); + } + .text-blue-700 { + color: var(--color-blue-700); + } .text-card-foreground { color: var(--card-foreground); } @@ -1566,9 +1971,39 @@ .text-destructive { color: var(--destructive); } + .text-emerald-700 { + color: var(--color-emerald-700); + } .text-foreground { color: var(--foreground); } + .text-gray-400 { + color: var(--color-gray-400); + } + .text-gray-500 { + color: var(--color-gray-500); + } + .text-gray-600 { + color: var(--color-gray-600); + } + .text-gray-700 { + color: var(--color-gray-700); + } + .text-gray-900 { + color: var(--color-gray-900); + } + .text-green-400 { + color: var(--color-green-400); + } + .text-green-500 { + color: var(--color-green-500); + } + .text-green-700 { + color: var(--color-green-700); + } + .text-inherit { + color: inherit; + } .text-muted-foreground { color: var(--muted-foreground); } @@ -1581,6 +2016,9 @@ .text-primary-foreground { color: var(--primary-foreground); } + .text-red-700 { + color: var(--color-red-700); + } .text-secondary-foreground { color: var(--secondary-foreground); } @@ -1593,9 +2031,21 @@ color: color-mix(in oklab, var(--sidebar-foreground) 70%, transparent); } } + .text-success { + color: var(--success); + } + .text-warning { + color: var(--warning); + } .text-white { color: var(--color-white); } + .text-yellow-700 { + color: var(--color-yellow-700); + } + .text-zinc-900 { + color: var(--color-zinc-900); + } .capitalize { text-transform: capitalize; } @@ -1783,6 +2233,10 @@ --tw-ease: linear; transition-timing-function: linear; } + .ease-out { + --tw-ease: var(--ease-out); + transition-timing-function: var(--ease-out); + } .fade-in-0 { --tw-enter-opacity: calc(0/100); --tw-enter-opacity: 0; @@ -1802,6 +2256,24 @@ .\[--cell-size\:--spacing\(8\)\] { --cell-size: calc(var(--spacing) * 8); } + .\[appkit\:cache\] { + appkit: cache; + } + .\[appkit\:connector\] { + appkit: connector; + } + .\[appkit\:my-plugin\] { + appkit: my-plugin; + } + .\[appkit\:server\] { + appkit: server; + } + .\[appkit\:test-scope\] { + appkit: test-scope; + } + .\[appkit\:test\] { + appkit: test; + } .running { animation-play-state: running; } @@ -2296,6 +2768,13 @@ } } } + .hover\:bg-blue-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-100); + } + } + } .hover\:bg-destructive\/90 { &:hover { @media (hover: hover) { @@ -2306,6 +2785,27 @@ } } } + .hover\:bg-emerald-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-emerald-100); + } + } + } + .hover\:bg-gray-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-100); + } + } + } + .hover\:bg-green-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-green-100); + } + } + } .hover\:bg-muted { &:hover { @media (hover: hover) { @@ -2333,6 +2833,13 @@ } } } + .hover\:bg-red-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-100); + } + } + } .hover\:bg-secondary\/80 { &:hover { @media (hover: hover) { @@ -2350,6 +2857,13 @@ } } } + .hover\:bg-yellow-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-yellow-100); + } + } + } .hover\:text-accent-foreground { &:hover { @media (hover: hover) { @@ -2357,6 +2871,27 @@ } } } + .hover\:text-blue-700 { + &:hover { + @media (hover: hover) { + color: var(--color-blue-700); + } + } + } + .hover\:text-blue-800 { + &:hover { + @media (hover: hover) { + color: var(--color-blue-800); + } + } + } + .hover\:text-emerald-700 { + &:hover { + @media (hover: hover) { + color: var(--color-emerald-700); + } + } + } .hover\:text-foreground { &:hover { @media (hover: hover) { @@ -2364,6 +2899,20 @@ } } } + .hover\:text-gray-700 { + &:hover { + @media (hover: hover) { + color: var(--color-gray-700); + } + } + } + .hover\:text-green-700 { + &:hover { + @media (hover: hover) { + color: var(--color-green-700); + } + } + } .hover\:text-muted-foreground { &:hover { @media (hover: hover) { @@ -2371,6 +2920,20 @@ } } } + .hover\:text-red-700 { + &:hover { + @media (hover: hover) { + color: var(--color-red-700); + } + } + } + .hover\:text-secondary-foreground { + &:hover { + @media (hover: hover) { + color: var(--secondary-foreground); + } + } + } .hover\:text-sidebar-accent-foreground { &:hover { @media (hover: hover) { @@ -2378,6 +2941,13 @@ } } } + .hover\:text-yellow-700 { + &:hover { + @media (hover: hover) { + color: var(--color-yellow-700); + } + } + } .hover\:underline { &:hover { @media (hover: hover) { @@ -2385,6 +2955,13 @@ } } } + .hover\:opacity-80 { + &:hover { + @media (hover: hover) { + opacity: 80%; + } + } + } .hover\:opacity-100 { &:hover { @media (hover: hover) { @@ -2400,6 +2977,14 @@ } } } + .hover\:shadow-lg { + &:hover { + @media (hover: hover) { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } .hover\:ring-4 { &:hover { @media (hover: hover) { @@ -2563,6 +3148,12 @@ outline-color: var(--ring); } } + .focus-visible\:outline-none { + &:focus-visible { + --tw-outline-style: none; + outline-style: none; + } + } .active\:bg-sidebar-accent { &:active { background-color: var(--sidebar-accent); @@ -3883,6 +4474,16 @@ position: absolute; } } + .md\:col-span-2 { + @media (width >= 48rem) { + grid-column: span 2 / span 2; + } + } + .md\:col-span-3 { + @media (width >= 48rem) { + grid-column: span 3 / span 3; + } + } .md\:block { @media (width >= 48rem) { display: block; @@ -3923,6 +4524,11 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } } + .md\:grid-cols-3 { + @media (width >= 48rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } .md\:flex-row { @media (width >= 48rem) { flex-direction: row; @@ -4010,6 +4616,11 @@ width: 600px; } } + .lg\:grid-cols-2 { + @media (width >= 64rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } .lg\:grid-cols-\[\.75fr_1fr\] { @media (width >= 64rem) { grid-template-columns: .75fr 1fr; @@ -4365,6 +4976,35 @@ width: calc(var(--spacing) * 5); } } + .\[\&_a\]\:underline { + & a { + text-decoration-line: underline; + } + } + .\[\&_code\]\:rounded { + & code { + border-radius: var(--radius); + } + } + .\[\&_code\]\:bg-background\/50 { + & code { + background-color: var(--background); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--background) 50%, transparent); + } + } + } + .\[\&_code\]\:px-1 { + & code { + padding-inline: calc(var(--spacing) * 1); + } + } + .\[\&_code\]\:text-xs { + & code { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + } .\[\&_img\]\:size-full { & img { width: 100%; @@ -4376,12 +5016,82 @@ object-fit: cover; } } + .\[\&_li\]\:my-0 { + & li { + margin-block: calc(var(--spacing) * 0); + } + } + .\[\&_ol\]\:my-1 { + & ol { + margin-block: calc(var(--spacing) * 1); + } + } + .\[\&_p\]\:my-1 { + & p { + margin-block: calc(var(--spacing) * 1); + } + } .\[\&_p\]\:leading-relaxed { & p { --tw-leading: var(--leading-relaxed); line-height: var(--leading-relaxed); } } + .\[\&_pre\]\:m-0 { + & pre { + margin: calc(var(--spacing) * 0); + } + } + .\[\&_pre\]\:min-h-\[300px\] { + & pre { + min-height: 300px; + } + } + .\[\&_pre\]\:overflow-x-auto { + & pre { + overflow-x: auto; + } + } + .\[\&_pre\]\:rounded { + & pre { + border-radius: var(--radius); + } + } + .\[\&_pre\]\:rounded-md { + & pre { + border-radius: var(--radius-md); + } + } + .\[\&_pre\]\:bg-background\/50 { + & pre { + background-color: var(--background); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--background) 50%, transparent); + } + } + } + .\[\&_pre\]\:p-2 { + & pre { + padding: calc(var(--spacing) * 2); + } + } + .\[\&_pre\]\:p-4 { + & pre { + padding: calc(var(--spacing) * 4); + } + } + .\[\&_pre\]\:text-sm { + & pre { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + } + .\[\&_pre\]\:text-xs { + & pre { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + } .\[\&_svg\]\:pointer-events-none { & svg { pointer-events: none; @@ -4415,6 +5125,59 @@ color: var(--muted-foreground); } } + .\[\&_table\]\:border-collapse { + & table { + border-collapse: collapse; + } + } + .\[\&_table\]\:text-xs { + & table { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + } + .\[\&_td\]\:border { + & td { + border-style: var(--tw-border-style); + border-width: 1px; + } + } + .\[\&_td\]\:border-border { + & td { + border-color: var(--border); + } + } + .\[\&_td\]\:px-2 { + & td { + padding-inline: calc(var(--spacing) * 2); + } + } + .\[\&_td\]\:py-1 { + & td { + padding-block: calc(var(--spacing) * 1); + } + } + .\[\&_th\]\:border { + & th { + border-style: var(--tw-border-style); + border-width: 1px; + } + } + .\[\&_th\]\:border-border { + & th { + border-color: var(--border); + } + } + .\[\&_th\]\:px-2 { + & th { + padding-inline: calc(var(--spacing) * 2); + } + } + .\[\&_th\]\:py-1 { + & th { + padding-block: calc(var(--spacing) * 1); + } + } .\[\&_tr\]\:border-b { & tr { border-bottom-style: var(--tw-border-style); @@ -4427,6 +5190,11 @@ border-width: 0px; } } + .\[\&_ul\]\:my-1 { + & ul { + margin-block: calc(var(--spacing) * 1); + } + } .\[\&\+\[data-slot\=item-content\]\]\:flex-none { &+[data-slot=item-content] { flex: none; @@ -4732,6 +5500,22 @@ border-radius: calc(var(--radius) - 5px); } } + .\[\&\>pre\]\:m-0 { + &>pre { + margin: calc(var(--spacing) * 0); + } + } + .\[\&\>pre\]\:p-4 { + &>pre { + padding: calc(var(--spacing) * 4); + } + } + .\[\&\>pre\]\:text-sm { + &>pre { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + } .\[\&\>span\]\:text-xs { &>span { font-size: var(--text-xs); diff --git a/packages/appkit-ui/package.json b/packages/appkit-ui/package.json index 6438e987..693acdc2 100644 --- a/packages/appkit-ui/package.json +++ b/packages/appkit-ui/package.json @@ -81,6 +81,7 @@ "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^0.554.0", + "marked": "^17.0.3", "next-themes": "^0.4.6", "react-day-picker": "^9.11.3", "react-hook-form": "^7.68.0", diff --git a/packages/appkit-ui/src/react/genie/genie-chat-input.tsx b/packages/appkit-ui/src/react/genie/genie-chat-input.tsx new file mode 100644 index 00000000..763486e1 --- /dev/null +++ b/packages/appkit-ui/src/react/genie/genie-chat-input.tsx @@ -0,0 +1,74 @@ +import { type KeyboardEvent, useRef, useState } from "react"; +import { cn } from "../lib/utils"; +import { Button } from "../ui/button"; + +export interface GenieChatInputProps { + onSend: (content: string) => void; + disabled?: boolean; + placeholder?: string; + className?: string; +} + +export function GenieChatInput({ + onSend, + disabled = false, + placeholder = "Ask a question...", + className, +}: GenieChatInputProps) { + const [value, setValue] = useState(""); + const textareaRef = useRef(null); + + const handleSubmit = () => { + const trimmed = value.trim(); + if (!trimmed || disabled) return; + onSend(trimmed); + setValue(""); + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; + + const handleInput = () => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = "auto"; + textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; + } + }; + + return ( +
+