lightnode-sdk 0.8.3 → 0.8.5

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/dist/add.d.ts CHANGED
@@ -119,7 +119,7 @@ export interface LayoutPatch {
119
119
  * Returns what happened so the CLI can report it; never throws.
120
120
  */
121
121
  export declare function patchLayoutWithProviders(cwd?: string): LayoutPatch;
122
- export declare const SCAFFOLD_GLOBALS_CSS = "@import \"tailwindcss\";\n\n/* dark mode via .dark class (we default the app to dark) */\n@custom-variant dark (&:is(.dark, .dark *));\n\n/* design tokens (light) - ported from lcai-chat-v2 */\n:root {\n font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif;\n\n --background: #ffffff;\n --primary: #6767e9;\n --primary-600: #5a4fd8;\n --foreground: #09090b;\n --card: #ffffff;\n --card-foreground: #09090b;\n --popover: #ffffff;\n --popover-foreground: hsl(240 10% 3.9%);\n --primary-foreground: #fafafa;\n --secondary: hsl(240 4.8% 95.9%);\n --secondary-foreground: hsl(240 5.9% 10%);\n --muted: hsl(240 4.8% 95.9%);\n --muted-foreground: hsl(240 3.8% 46.1%);\n --accent: hsl(240 4.8% 95.9%);\n --accent-foreground: hsl(240 5.9% 10%);\n --destructive: #ef4d6a;\n --destructive-foreground: hsl(0 0% 98%);\n --success: #15bd77;\n --warning: #eaa53d;\n --border: hsl(240 5.9% 90%);\n --input: hsl(240 5.9% 90%);\n --ring: hsl(240 10% 3.9%);\n --radius: 0.625rem;\n\n --surface-base-subtle: rgba(34, 35, 42, 0.02);\n --surface-base-faint: rgba(14, 18, 27, 0.04);\n --surface-base-light: rgba(204, 206, 239, 0.16);\n --surface-elevation-light: #ffffff;\n\n --content-primary: #0f0f14;\n --content-default: #373842;\n --content-soft: #656678;\n --content-extraLight: #9798b6;\n\n --border-soft: rgba(14, 18, 27, 0.08);\n --border-light: rgba(14, 18, 27, 0.06);\n}\n\n/* design tokens (dark) */\n.dark {\n --background: #070710;\n --foreground: hsl(0 0% 98%);\n --card: #0f0f14;\n --card-foreground: hsl(0 0% 98%);\n --popover: #0f0f14;\n --popover-foreground: hsl(0 0% 98%);\n --primary: #7064e9;\n --primary-600: #8c71f6;\n --primary-foreground: hsl(0 0% 98%);\n --secondary: hsl(240 3.7% 15.9%);\n --secondary-foreground: hsl(0 0% 98%);\n --muted: hsl(240 3.7% 15.9%);\n --muted-foreground: hsl(240 5% 64.9%);\n --accent: hsl(240 3.7% 15.9%);\n --accent-foreground: hsl(0 0% 98%);\n --destructive: #fb5a76;\n --destructive-foreground: hsl(0 0% 98%);\n --success: #22d68a;\n --warning: #f5be5c;\n --border: hsl(240 3.7% 15.9%);\n --input: hsl(240 3.7% 15.9%);\n --ring: hsl(240 4.9% 83.9%);\n\n --surface-base-subtle: rgba(204, 206, 239, 0.02);\n --surface-base-faint: rgba(204, 206, 239, 0.04);\n --surface-base-light: rgba(204, 206, 239, 0.08);\n --surface-elevation-light: #0f0f14;\n\n --content-primary: #cccef0;\n --content-default: #9798b6;\n --content-soft: rgba(154, 156, 207, 0.8);\n --content-extraLight: #9798b6;\n\n --border-soft: rgba(204, 206, 239, 0.12);\n --border-light: rgba(204, 206, 239, 0.08);\n}\n\n/* theme mapping (Tailwind v4 @theme) */\n@theme inline {\n --radius-md: calc(var(--radius) - 2px);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-lg: var(--radius);\n\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-card: var(--card);\n --color-card-foreground: var(--card-foreground);\n --color-popover: var(--popover);\n --color-popover-foreground: var(--popover-foreground);\n --color-primary: var(--primary);\n --color-primary-600: var(--primary-600);\n --color-primary-foreground: var(--primary-foreground);\n --color-secondary: var(--secondary);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-muted: var(--muted);\n --color-muted-foreground: var(--muted-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --color-destructive: var(--destructive);\n --color-destructive-foreground: var(--destructive-foreground);\n --color-success: var(--success);\n --color-warning: var(--warning);\n --color-border: var(--border);\n --color-input: var(--input);\n --color-ring: var(--ring);\n\n --color-surface-base-subtle: var(--surface-base-subtle);\n --color-surface-base-faint: var(--surface-base-faint);\n --color-surface-base-light: var(--surface-base-light);\n --color-surface-elevation-light: var(--surface-elevation-light);\n --color-surface-base-brand-default: #693ee0;\n --color-surface-base-brand-strong: #8c71f6;\n\n --color-content-primary: var(--content-primary);\n --color-content-default: var(--content-default);\n --color-content-soft: var(--content-soft);\n --color-content-extraLight: var(--content-extraLight);\n\n --color-bdr-soft: var(--border-soft);\n --color-bdr-light: var(--border-light);\n\n --color-gradient-primary: linear-gradient(270deg, #7064e9 0%, #dd00ac 100%);\n}\n\n@layer base {\n * {\n border-color: var(--border);\n }\n body {\n background-color: var(--background);\n color: var(--foreground);\n overflow-x: hidden;\n }\n html {\n overflow-x: hidden;\n }\n button {\n cursor: pointer;\n }\n button:disabled {\n cursor: not-allowed;\n }\n /* visible keyboard focus across interactive elements */\n a:focus-visible,\n button:focus-visible,\n input:focus-visible,\n select:focus-visible,\n textarea:focus-visible {\n outline: 2px solid var(--primary);\n outline-offset: 2px;\n border-radius: 6px;\n }\n}\n\n/* respect reduced-motion: kill non-essential animation */\n@media (prefers-reduced-motion: reduce) {\n *,\n ::before,\n ::after {\n animation-duration: 0.001ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.001ms !important;\n scroll-behavior: auto !important;\n }\n}\n\n/* ambient app background (gradient mesh behind everything) */\nbody::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: -1;\n pointer-events: none;\n background:\n radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.10), transparent 60%),\n radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.14), transparent 60%),\n radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.12), transparent 60%),\n radial-gradient(45% 45% at 50% 115%, rgba(112, 100, 233, 0.07), transparent 60%);\n}\n.dark body::before {\n background:\n radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.12), transparent 60%),\n radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.18), transparent 60%),\n radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.14), transparent 60%),\n radial-gradient(45% 45% at 50% 118%, rgba(112, 100, 233, 0.10), transparent 60%);\n}\n\n/* signature lcai gradient (primary buttons / accents) */\n.bg-gradient-primary {\n background-image: var(--color-gradient-primary);\n}\n.text-gradient {\n background: linear-gradient(94deg, #dd00ac 10%, #7130c3 53%, #7064e9 96%);\n -webkit-background-clip: text;\n background-clip: text;\n color: transparent;\n}\n\n/* minimal scrollbar */\n::-webkit-scrollbar {\n width: 6px;\n height: 6px;\n}\n::-webkit-scrollbar-track {\n background: transparent;\n}\n::-webkit-scrollbar-thumb {\n background: var(--border);\n border-radius: 3px;\n}\n* {\n scrollbar-width: thin;\n scrollbar-color: var(--border) transparent;\n}\n";
122
+ export declare const SCAFFOLD_GLOBALS_CSS = "@import \"tailwindcss\";\n\n/* let Tailwind v4 see streamdown's classes so markdown answers are styled\n (harmless when streamdown isn't installed - the path just matches nothing) */\n@source \"../node_modules/streamdown/dist/index.js\";\n\n/* dark mode via .dark class (we default the app to dark) */\n@custom-variant dark (&:is(.dark, .dark *));\n\n/* design tokens (light) - ported from lcai-chat-v2 */\n:root {\n font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif;\n\n --background: #ffffff;\n --primary: #6767e9;\n --primary-600: #5a4fd8;\n --foreground: #09090b;\n --card: #ffffff;\n --card-foreground: #09090b;\n --popover: #ffffff;\n --popover-foreground: hsl(240 10% 3.9%);\n --primary-foreground: #fafafa;\n --secondary: hsl(240 4.8% 95.9%);\n --secondary-foreground: hsl(240 5.9% 10%);\n --muted: hsl(240 4.8% 95.9%);\n --muted-foreground: hsl(240 3.8% 46.1%);\n --accent: hsl(240 4.8% 95.9%);\n --accent-foreground: hsl(240 5.9% 10%);\n --destructive: #ef4d6a;\n --destructive-foreground: hsl(0 0% 98%);\n --success: #15bd77;\n --warning: #eaa53d;\n --border: hsl(240 5.9% 90%);\n --input: hsl(240 5.9% 90%);\n --ring: hsl(240 10% 3.9%);\n --radius: 0.625rem;\n\n --surface-base-subtle: rgba(34, 35, 42, 0.02);\n --surface-base-faint: rgba(14, 18, 27, 0.04);\n --surface-base-light: rgba(204, 206, 239, 0.16);\n --surface-elevation-light: #ffffff;\n\n --content-primary: #0f0f14;\n --content-default: #373842;\n --content-soft: #656678;\n --content-extraLight: #9798b6;\n\n --border-soft: rgba(14, 18, 27, 0.08);\n --border-light: rgba(14, 18, 27, 0.06);\n}\n\n/* design tokens (dark) */\n.dark {\n --background: #070710;\n --foreground: hsl(0 0% 98%);\n --card: #0f0f14;\n --card-foreground: hsl(0 0% 98%);\n --popover: #0f0f14;\n --popover-foreground: hsl(0 0% 98%);\n --primary: #7064e9;\n --primary-600: #8c71f6;\n --primary-foreground: hsl(0 0% 98%);\n --secondary: hsl(240 3.7% 15.9%);\n --secondary-foreground: hsl(0 0% 98%);\n --muted: hsl(240 3.7% 15.9%);\n --muted-foreground: hsl(240 5% 64.9%);\n --accent: hsl(240 3.7% 15.9%);\n --accent-foreground: hsl(0 0% 98%);\n --destructive: #fb5a76;\n --destructive-foreground: hsl(0 0% 98%);\n --success: #22d68a;\n --warning: #f5be5c;\n --border: hsl(240 3.7% 15.9%);\n --input: hsl(240 3.7% 15.9%);\n --ring: hsl(240 4.9% 83.9%);\n\n --surface-base-subtle: rgba(204, 206, 239, 0.02);\n --surface-base-faint: rgba(204, 206, 239, 0.04);\n --surface-base-light: rgba(204, 206, 239, 0.08);\n --surface-elevation-light: #0f0f14;\n\n --content-primary: #cccef0;\n --content-default: #9798b6;\n --content-soft: rgba(154, 156, 207, 0.8);\n --content-extraLight: #9798b6;\n\n --border-soft: rgba(204, 206, 239, 0.12);\n --border-light: rgba(204, 206, 239, 0.08);\n}\n\n/* theme mapping (Tailwind v4 @theme) */\n@theme inline {\n --radius-md: calc(var(--radius) - 2px);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-lg: var(--radius);\n\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-card: var(--card);\n --color-card-foreground: var(--card-foreground);\n --color-popover: var(--popover);\n --color-popover-foreground: var(--popover-foreground);\n --color-primary: var(--primary);\n --color-primary-600: var(--primary-600);\n --color-primary-foreground: var(--primary-foreground);\n --color-secondary: var(--secondary);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-muted: var(--muted);\n --color-muted-foreground: var(--muted-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --color-destructive: var(--destructive);\n --color-destructive-foreground: var(--destructive-foreground);\n --color-success: var(--success);\n --color-warning: var(--warning);\n --color-border: var(--border);\n --color-input: var(--input);\n --color-ring: var(--ring);\n\n --color-surface-base-subtle: var(--surface-base-subtle);\n --color-surface-base-faint: var(--surface-base-faint);\n --color-surface-base-light: var(--surface-base-light);\n --color-surface-elevation-light: var(--surface-elevation-light);\n --color-surface-base-brand-default: #693ee0;\n --color-surface-base-brand-strong: #8c71f6;\n\n --color-content-primary: var(--content-primary);\n --color-content-default: var(--content-default);\n --color-content-soft: var(--content-soft);\n --color-content-extraLight: var(--content-extraLight);\n\n --color-bdr-soft: var(--border-soft);\n --color-bdr-light: var(--border-light);\n\n --color-gradient-primary: linear-gradient(270deg, #7064e9 0%, #dd00ac 100%);\n}\n\n@layer base {\n * {\n border-color: var(--border);\n }\n body {\n background-color: var(--background);\n color: var(--foreground);\n overflow-x: hidden;\n }\n html {\n overflow-x: hidden;\n }\n button {\n cursor: pointer;\n }\n button:disabled {\n cursor: not-allowed;\n }\n /* visible keyboard focus across interactive elements */\n a:focus-visible,\n button:focus-visible,\n input:focus-visible,\n select:focus-visible,\n textarea:focus-visible {\n outline: 2px solid var(--primary);\n outline-offset: 2px;\n border-radius: 6px;\n }\n}\n\n/* respect reduced-motion: kill non-essential animation */\n@media (prefers-reduced-motion: reduce) {\n *,\n ::before,\n ::after {\n animation-duration: 0.001ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.001ms !important;\n scroll-behavior: auto !important;\n }\n}\n\n/* ambient app background (gradient mesh behind everything) */\nbody::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: -1;\n pointer-events: none;\n background:\n radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.10), transparent 60%),\n radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.14), transparent 60%),\n radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.12), transparent 60%),\n radial-gradient(45% 45% at 50% 115%, rgba(112, 100, 233, 0.07), transparent 60%);\n}\n.dark body::before {\n background:\n radial-gradient(60% 50% at 50% -6%, rgba(221, 0, 172, 0.12), transparent 60%),\n radial-gradient(55% 45% at 12% -8%, rgba(112, 100, 233, 0.18), transparent 60%),\n radial-gradient(50% 40% at 88% -2%, rgba(112, 100, 233, 0.14), transparent 60%),\n radial-gradient(45% 45% at 50% 118%, rgba(112, 100, 233, 0.10), transparent 60%);\n}\n\n/* signature lcai gradient (primary buttons / accents) */\n.bg-gradient-primary {\n background-image: var(--color-gradient-primary);\n}\n/* the lcai-chat connect-button gradient (pink -> purple) */\n.bg-gradient-btn {\n background-image: linear-gradient(94deg, #dd00ac 10.66%, #7130c3 53.03%, #410093 96.34%);\n background-size: 200% auto;\n}\n@keyframes pulse-dot {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n.animate-pulse-dot {\n animation: pulse-dot 1.6s ease-in-out infinite;\n}\n.text-gradient {\n background: linear-gradient(94deg, #dd00ac 10%, #7130c3 53%, #7064e9 96%);\n -webkit-background-clip: text;\n background-clip: text;\n color: transparent;\n}\n\n/* minimal scrollbar */\n::-webkit-scrollbar {\n width: 6px;\n height: 6px;\n}\n::-webkit-scrollbar-track {\n background: transparent;\n}\n::-webkit-scrollbar-thumb {\n background: var(--border);\n border-radius: 3px;\n}\n* {\n scrollbar-width: thin;\n scrollbar-color: var(--border) transparent;\n}\n";
123
123
  export interface ScaffoldWiring {
124
124
  written: WrittenFile[];
125
125
  /** The route now also served at `/` (e.g. "/chat-web3"), or null if nothing was wired. */
package/dist/add.js CHANGED
@@ -961,14 +961,16 @@ const NEXTJS_CHAT_WEB3_PAGE = `// app/chat-web3/page.tsx
961
961
  // turn plus a small gas amount.
962
962
  "use client";
963
963
 
964
- import { useEffect, useState } from "react";
964
+ import { useEffect, useRef, useState } from "react";
965
965
  import { useAccount, useWalletClient, usePublicClient } from "wagmi";
966
966
  import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
967
+ import { Streamdown } from "streamdown";
967
968
  import { ConnectButton } from "@/components/connect-button";
968
969
 
969
970
  type Turn = {
970
971
  role: "user" | "assistant";
971
972
  text: string;
973
+ streaming?: boolean;
972
974
  worker?: string | null;
973
975
  jobId?: string | null;
974
976
  submitTx?: \`0x\${string}\` | null;
@@ -977,6 +979,30 @@ type Turn = {
977
979
 
978
980
  const MODEL = "llama3-8b";
979
981
 
982
+ /** The LightChain atom mark (gradient logo). No wordmark - the viewBox frames
983
+ * the atom only. Used as the assistant avatar and the input glyph. */
984
+ function LcaiMark({ className }: { className?: string }) {
985
+ return (
986
+ <svg
987
+ viewBox="854.3951253356933 398.23541259765625 186.60832495117188 198.4013696411746"
988
+ className={className}
989
+ xmlns="http://www.w3.org/2000/svg"
990
+ aria-hidden="true"
991
+ >
992
+ <g transform="translate(856.3148789999999, 398.23539999999997) scale(0.6266964564006055)">
993
+ <linearGradient gradientTransform="matrix(1 0 0 1 0 -244)" gradientUnits="userSpaceOnUse" id="lcai-mark-grad" x1="-0.0000002" x2="298.8796082" y1="401.5" y2="401.5">
994
+ <stop offset="0" stopColor="rgb(48, 5, 250)" />
995
+ <stop offset="1" stopColor="rgb(255, 18, 251)" />
996
+ </linearGradient>
997
+ <path
998
+ fill="url(#lcai-mark-grad)"
999
+ d="M280.0912476,168.2428284c0.0187378-0.036911,0.0380554-0.0726929,0.0562134-0.1096191c16.2681885-32.6010895,17.265564-64.3445282,2.8092651-89.3834915c-12.8778992-22.3064003-36.5042877-36.642292-67.219101-41.070858C196.544342,13.2930756,172.316452,0,146.5589905,0c-22.3245773,0-43.5001221,9.986846-61.2456131,28.5235424C59.731987,30.5614643,38.6132431,40.6585007,24.5028381,57.9581261C6.2308226,80.3639221,2.1413448,111.8585815,12.9886856,146.6407318c0.0119276,0.0391846,0.0255585,0.0778198,0.0380545,0.1170044c-0.0181751,0.0363464-0.0380545,0.0727081-0.0562305,0.1090546c-16.2681713,32.6010895-17.2655487,64.3445282-2.8092442,89.3834991c12.8778811,22.3069611,36.5042725,36.6422882,67.2190933,41.0714264C96.5736389,301.7069092,120.8015289,315,146.5589905,315c22.3296814,0,43.5103455-9.991394,61.2581024-28.5365906c25.5706024-2.0402222,46.6916199-12.125885,60.7980499-29.4215393c18.2720032-22.4057922,22.3614807-53.9004517,11.5141602-88.6826019C280.1173706,168.3206482,280.1037292,168.2814484,280.0912476,168.2428284z M271.8764954,85.1474762c10.6303711,18.4145813,11.1694031,41.5411453,1.7635803,66.0655212c-3.7219849-8.3783264-8.2141418-16.6447449-13.4100647-24.7055588l-4.6642761,17.0429001c4.3473511,7.8126068,7.9301758,15.6831512,10.7121582,23.4855347c-4.3819885,8.0193481-9.6506042,15.8308258-15.7041626,23.3338776c1.5176544-10.6928558,2.3207703-21.6884308,2.3207703-32.8691864c0-29.667511-5.4985657-60.093277-17.7237091-87.3391113c-2.6733246-5.9579468-5.6783905-11.7689896-9.0385132-17.369133C246.8721771,58.2199669,262.7427673,69.3285828,271.8764954,85.1474762z M53.0189972,157.5005646c0-17.7983093,2.0930176-34.8525772,5.9098625-50.615242c11.8929939-11.332962,25.6574974-21.6162949,40.8601341-30.3939056c17.8886261-10.3276367,36.5854874-17.8778381,55.0960617-22.4290848c17.2502136,7.3440208,34.5396118,17.6932373,50.7930145,30.9488297c11.4465637,9.3348007,21.5856323,19.4636459,30.3081512,30.0235825c2.6712189,13.4345093,4.1127625,27.6942902,4.1127625,42.4658203c0,17.79776-2.0930176,34.852005-5.9092865,50.6141052c-11.8930054,11.3329773-25.6575012,21.6163025-40.8607025,30.3939209c-17.8818207,10.3236542-36.5724335,17.8908844-55.0767517,22.4427032c-17.2558975-7.3440094-34.5521164-17.7017517-50.811203-30.9624481h-0.0011368c-11.4465637-9.3348083-21.5856247-19.4636536-30.3081436-30.0235901C54.460537,186.531311,53.0189972,172.27211,53.0189972,157.5005646z M197.5405884,36.2367516c-8.420929-0.1454048-17.0355225,0.3947487-25.7472534,1.5812645l10.6093597,11.7066994c4.158783-0.3368149,8.2834625-0.5191383,12.357605-0.5191383c2.894455,0,5.7661743,0.0851974,8.6072235,0.2578659c1.8720703,0.1141663,3.7151794,0.2686577,5.5338593,0.456089c7.9432373,11.0063782,14.6692963,24.0733986,19.8174896,38.6586342c-4.7466278-4.5887375-9.7346497-9.0269547-14.9538574-13.283989c-5.9595642-4.8602829-12.1525574-9.4351349-18.5579681-13.6909294c-6.3523102-4.2205162-12.9407349-7.9979477-19.6017914-11.7028999c-4.8133698-2.6772537-9.9029083-5.0026283-14.9892578-7.1101379c-1.8141327-0.7520103-3.3374634-1.3887177-4.6540527-1.9288712c-15.6155548-6.2455406-31.4423981-10.2702694-46.9034653-11.8191586c-1.6102371-0.1607399-3.2039948-0.2845592-4.789238-0.3907719c12.707489-10.0067272,27.0797272-15.6558857,42.2897491-15.6558857C165.352417,12.7955227,182.865036,21.4209137,197.5405884,36.2367516z M34.4186859,66.0450745c9.6119766-11.7850838,23.0726089-19.3455086,38.8551559-22.8698425c-0.9178619,1.2932968-1.8266373,2.6036339-2.717804,3.9503212c-7.5609894,11.4329338-13.7923317,24.314785-18.6122322,38.2065163l15.7944679-5.6764221c6.2233963-15.0276947,14.1956024-28.1611671,23.4639511-38.7228127c1.3574829-0.0494118,2.7212067-0.0846291,4.0997009-0.0846291c12.3808975,0,25.5910492,1.9907799,39.0925751,5.8922577c-13.8752594,4.7176666-27.6653214,10.9700241-41.0032654,18.6707382c-9.0180283,5.2061462-17.6032944,11.0505371-25.9167862,17.3136139c-6.0140381,4.5307617-12.0375252,9.2800751-17.314682,14.6695099c-0.9797707,0.965004-1.8680954,1.8391342-2.6734962,2.6308975c-0.0170364,0.0653229-0.0329399,0.1312103-0.0505486,0.1965256c-9.7846451,9.6653671-18.3208618,20.0276489-25.3808918,30.852272C16.8799381,106.224762,20.5229797,83.083992,34.4186859,66.0450745z M21.2414799,229.8525238c-10.630372-18.4140167-11.1693878-41.5405731-1.763588-66.0655212c4.1763802,9.4012604,9.3228741,18.6616516,15.3446293,27.6482849l4.0724411-17.6199799c-4.9550858-8.587326-8.9911728-17.2598572-12.054306-25.851181c4.381422-8.0193481,9.650032-15.8308258,15.7035942-23.3338776c-1.5176506,10.6934204-2.3207779,21.6895752-2.3207779,32.8703156c0,7.742981,0.3608093,15.5649719,1.0776787,23.2746582c2.6225128,28.2041779,11.0045586,56.9674835,25.6845512,81.4324493C46.2463646,256.7800293,30.3752155,245.6719818,21.2414799,229.8525238z M95.5614929,278.7479248c0.9189987,0.0158997,1.8345871,0.0408936,2.7575607,0.0408936c7.9733429,0,16.1102676-0.612854,24.3289871-1.7931213l-10.7837296-11.6033325c-7.5093002,0.662262-14.9044418,0.7838135-22.1138535,0.3441772c-1.8720703-0.1135864-3.7151718-0.2680664-5.5338593-0.4555054c-7.9438095-11.0069427-14.6698608-24.0739594-19.8180618-38.6603394c4.7472,4.5893097,9.7357941,9.0280914,14.9555588,13.2851257h-0.0011292c11.9242325,9.7244415,24.5255051,18.0942383,37.4562073,24.9662781c-0.021019,0.0028381-0.0420303,0.0050964-0.0630493,0.0073547c5.3225708,2.7774658,11.9242401,6.0115356,18.2651978,8.5464478c2.2242279,0.8894653,4.0071259,1.6102295,5.4679718,2.2071838c14.5341187,5.5088806,29.2147827,9.0860291,43.5802155,10.5247192c1.6107941,0.1613159,3.2039948,0.2845764,4.7897949,0.3907776c-12.7080536,10.0067444-27.0802917,15.6558838-42.2903137,15.6558838C127.7593155,302.2044678,110.240448,293.5728455,95.5614929,278.7479248z M258.6993103,248.9549255c-9.6131287,11.7862244-23.0743256,19.3534546-38.8602753,22.8778076c0.9195557-1.2955933,1.8305969-2.6087646,2.7229004-3.9582825c7.3122253-11.0557861,13.3947449-23.4577026,18.1470642-36.8263245l-15.8035583,5.4151459c-6.1529694,14.5613861-13.9542084,27.3091888-22.9919586,37.6073151c-13.5583191,0.4913025-28.2117157-1.4767761-43.229187-5.8201294c13.8883209-4.7193909,27.6931458-10.9535522,41.04245-18.6616669c9.6801453-5.5883789,18.8150177-11.7680359,27.3148804-18.427063c-0.0124969,0.0312347-0.0238647,0.0630493-0.0363464,0.0942841c4.5438538-3.6339569,9.8499603-8.1062622,14.4875336-12.6194458c3.7958374-3.6941681,6.0331268-5.820694,7.3400421-7.0401611c8.474884-8.7463684,15.9393005-18.0238037,22.2302856-27.6698608C276.2380371,208.775238,272.5950012,231.9165802,258.6993103,248.9549255z M151.8601074,99.7067947l2.0352783,43.1479874c0.1197662,2.5387726,2.1956635,4.5453033,4.7370453,4.5787506l30.6875916,0.4037781c3.7883453,0.0498505,6.0318604,4.2594757,3.9608765,7.4320374l-40.7515411,62.4279022c-2.6121979,4.0016479-8.8297119,2.1519318-8.8297119-2.6268463v-42.6512451c0-2.6540527-2.151535-4.8055878-4.805603-4.8055878h-32.7028122c-3.7936096,0-6.0953522-4.18367-4.056221-7.3826447c9.1347427-14.3305359,29.4064636-45.9985199,40.9515228-63.0014496C145.6810608,93.4083862,151.6424866,95.0932236,151.8601074,99.7067947z"
1000
+ />
1001
+ </g>
1002
+ </svg>
1003
+ );
1004
+ }
1005
+
980
1006
  export default function ChatWeb3() {
981
1007
  const { address, chain } = useAccount();
982
1008
  const network: "mainnet" | "testnet" | null =
@@ -990,6 +1016,7 @@ export default function ChatWeb3() {
990
1016
  const [busyStage, setBusyStage] = useState("");
991
1017
  const [err, setErr] = useState<string | null>(null);
992
1018
  const [feeLcai, setFeeLcai] = useState<number | null>(null);
1019
+ const endRef = useRef<HTMLDivElement>(null);
993
1020
 
994
1021
  // Read the on-chain fee for the connected network so we can show the
995
1022
  // visitor the real cost per turn before they click Send.
@@ -1003,6 +1030,11 @@ export default function ChatWeb3() {
1003
1030
  return () => { cancelled = true; };
1004
1031
  }, [network]);
1005
1032
 
1033
+ // Keep the latest turn (and the "writing on chain" indicator) in view.
1034
+ useEffect(() => {
1035
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
1036
+ }, [turns, busy]);
1037
+
1006
1038
  /** Build a single prompt from history + new user input. */
1007
1039
  function composePrompt(history: Turn[], next: string, system: string): string {
1008
1040
  const lines: string[] = [];
@@ -1014,6 +1046,16 @@ export default function ChatWeb3() {
1014
1046
  return system ? \`\${system}\\n\\n\${lines.join("\\n\\n")}\` : lines.join("\\n\\n");
1015
1047
  }
1016
1048
 
1049
+ /** Patch the trailing assistant turn (the streaming placeholder) in place. */
1050
+ function patchLastAssistant(patch: Partial<Turn>) {
1051
+ setTurns((prev) => {
1052
+ if (prev.length === 0) return prev;
1053
+ const last = prev[prev.length - 1];
1054
+ if (last.role !== "assistant") return prev;
1055
+ return [...prev.slice(0, -1), { ...last, ...patch }];
1056
+ });
1057
+ }
1058
+
1017
1059
  async function send() {
1018
1060
  if (!walletClient || !publicClient || !address || !network) {
1019
1061
  setErr("Connect a wallet on LightChain mainnet (9200) or testnet (8200) first.");
@@ -1024,8 +1066,8 @@ export default function ChatWeb3() {
1024
1066
  setBusy(true);
1025
1067
  setErr(null);
1026
1068
  const history = [...turns];
1027
- // Optimistic user bubble so it appears immediately.
1028
- setTurns([...history, { role: "user", text: next }]);
1069
+ // Optimistic user bubble + an empty assistant turn we stream tokens into.
1070
+ setTurns([...history, { role: "user", text: next }, { role: "assistant", text: "", streaming: true }]);
1029
1071
  setInput("");
1030
1072
  try {
1031
1073
  const system = "You are a concise assistant. Reply in one or two short sentences.";
@@ -1045,16 +1087,21 @@ export default function ChatWeb3() {
1045
1087
  model: MODEL,
1046
1088
  jobCompletedTimeoutMs: 120_000,
1047
1089
  maxRetries: 1,
1090
+ // Stream each decrypted chunk into the assistant bubble as it arrives.
1091
+ onChunk: (_chunk, totalSoFar) => {
1092
+ setBusyStage("");
1093
+ patchLastAssistant({ text: totalSoFar });
1094
+ },
1048
1095
  });
1049
1096
 
1050
- setTurns([...history, { role: "user", text: next }, {
1051
- role: "assistant",
1097
+ patchLastAssistant({
1052
1098
  text: result.answer,
1099
+ streaming: false,
1053
1100
  worker: result.worker,
1054
1101
  jobId: result.jobId?.toString() ?? null,
1055
1102
  submitTx: result.txs?.submitJob ?? null,
1056
1103
  jobCompletedTx: result.txs?.jobCompleted ?? null,
1057
- }]);
1104
+ });
1058
1105
  } catch (e) {
1059
1106
  // Roll back the optimistic user bubble so the visitor can retry.
1060
1107
  setTurns(history);
@@ -1074,10 +1121,10 @@ export default function ChatWeb3() {
1074
1121
  }
1075
1122
 
1076
1123
  return (
1077
- <main className="mx-auto flex min-h-screen w-full max-w-2xl flex-col px-4 py-6">
1078
- <header className="flex items-center justify-between gap-3 border-b border-border pb-4">
1124
+ <main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col px-4 py-6">
1125
+ <header className="flex items-center justify-between gap-3 pb-6">
1079
1126
  <div className="min-w-0">
1080
- <h1 className="text-base font-semibold text-foreground">Chat</h1>
1127
+ <h1 className="font-semibold text-foreground">Chat</h1>
1081
1128
  <p className="truncate text-xs text-muted-foreground">
1082
1129
  {network ? (
1083
1130
  <>Signed from your wallet on {network} · {feeLcai != null ? feeLcai + " LCAI" : "..."}/turn + gas</>
@@ -1089,10 +1136,13 @@ export default function ChatWeb3() {
1089
1136
  <ConnectButton />
1090
1137
  </header>
1091
1138
 
1092
- <div className="flex flex-1 flex-col gap-3 overflow-y-auto py-6">
1139
+ <div className="flex flex-1 flex-col gap-6 overflow-y-auto pb-6">
1093
1140
  {turns.length === 0 ? (
1094
- <div className="flex flex-1 flex-col items-center justify-center gap-3 text-center">
1095
- <h2 className="text-2xl font-medium text-foreground">How can I help?</h2>
1141
+ <div className="flex flex-1 flex-col items-center justify-center gap-4 text-center">
1142
+ <LcaiMark className="size-12" />
1143
+ <h2 className="text-3xl font-medium tracking-tight text-foreground">
1144
+ Start talking with <span className="text-gradient">AI Chat</span>
1145
+ </h2>
1096
1146
  <p className="max-w-sm text-sm text-muted-foreground">
1097
1147
  Connect your wallet and send a message. Each turn is signed and paid from your
1098
1148
  own wallet, no backend required.
@@ -1104,44 +1154,63 @@ export default function ChatWeb3() {
1104
1154
  )}
1105
1155
  </div>
1106
1156
  ) : (
1107
- turns.map((t, i) => (
1108
- <div
1109
- key={i}
1110
- className={
1111
- "max-w-[85%] whitespace-pre-wrap break-words rounded-2xl px-4 py-2.5 text-sm " +
1112
- (t.role === "user"
1113
- ? "self-end rounded-br-md bg-primary text-primary-foreground"
1114
- : "self-start rounded-bl-md border border-border bg-card text-foreground")
1115
- }
1116
- >
1117
- <div>{t.text}</div>
1118
- {t.role === "assistant" && t.submitTx ? (
1119
- <div className="mt-2 flex flex-wrap gap-3 text-[11px] text-muted-foreground">
1120
- {t.worker && (
1121
- <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/address/\${t.worker}\`} target="_blank" rel="noopener noreferrer">
1122
- worker
1123
- </a>
1157
+ turns.map((t, i) =>
1158
+ t.role === "user" ? (
1159
+ <div key={i} className="flex justify-end">
1160
+ <div className="w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl bg-surface-base-faint px-4 py-2.5 text-sm text-foreground">
1161
+ {t.text}
1162
+ </div>
1163
+ </div>
1164
+ ) : (
1165
+ <div key={i} className="group flex gap-3">
1166
+ <LcaiMark className="mt-0.5 size-7 shrink-0" />
1167
+ <div className="flex min-w-0 flex-1 flex-col gap-2">
1168
+ {t.text ? (
1169
+ <div className="max-w-none text-sm leading-relaxed text-foreground [&_*:first-child]:mt-0 [&_*:last-child]:mb-0">
1170
+ <Streamdown>{t.text}</Streamdown>
1171
+ </div>
1172
+ ) : (
1173
+ <div className="animate-pulse-dot pt-1 text-sm text-muted-foreground">
1174
+ {busyStage || "Writing on chain..."}
1175
+ </div>
1124
1176
  )}
1125
- {t.jobId && <span>job #{t.jobId}</span>}
1126
1177
  {t.submitTx && (
1127
- <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/tx/\${t.submitTx}\`} target="_blank" rel="noopener noreferrer">
1128
- submitJob
1129
- </a>
1130
- )}
1131
- {t.jobCompletedTx && (
1132
- <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/tx/\${t.jobCompletedTx}\`} target="_blank" rel="noopener noreferrer">
1133
- completed
1134
- </a>
1178
+ <div className="flex flex-wrap items-center gap-3 text-[11px] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100">
1179
+ <button
1180
+ type="button"
1181
+ onClick={() => navigator.clipboard?.writeText(t.text)}
1182
+ className="inline-flex items-center gap-1 hover:text-foreground"
1183
+ aria-label="Copy"
1184
+ >
1185
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1186
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
1187
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
1188
+ </svg>
1189
+ Copy
1190
+ </button>
1191
+ {t.worker && (
1192
+ <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/address/\${t.worker}\`} target="_blank" rel="noopener noreferrer">worker</a>
1193
+ )}
1194
+ {t.jobId && <span>job #{t.jobId}</span>}
1195
+ {t.submitTx && (
1196
+ <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/tx/\${t.submitTx}\`} target="_blank" rel="noopener noreferrer">submitJob</a>
1197
+ )}
1198
+ {t.jobCompletedTx && (
1199
+ <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/tx/\${t.jobCompletedTx}\`} target="_blank" rel="noopener noreferrer">completed</a>
1200
+ )}
1201
+ </div>
1135
1202
  )}
1136
1203
  </div>
1137
- ) : null}
1138
- </div>
1139
- ))
1204
+ </div>
1205
+ )
1206
+ )
1140
1207
  )}
1208
+ <div ref={endRef} />
1141
1209
  </div>
1142
1210
 
1143
- <div className="border-t border-border pt-4">
1144
- <div className="flex items-end gap-2 rounded-2xl border border-border bg-card p-2 transition focus-within:ring-2 focus-within:ring-primary">
1211
+ <div className="rounded-2xl border border-border bg-card p-3">
1212
+ <div className="flex items-start gap-2">
1213
+ <LcaiMark className="mt-2 size-5 shrink-0" />
1145
1214
  <textarea
1146
1215
  value={input}
1147
1216
  onChange={(e) => setInput(e.target.value)}
@@ -1149,16 +1218,31 @@ export default function ChatWeb3() {
1149
1218
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (!busy && input.trim()) send(); }
1150
1219
  }}
1151
1220
  rows={1}
1152
- placeholder={turns.length === 0 ? "Say hello (cmd+enter to send)" : "Reply... (cmd+enter)"}
1153
- className="max-h-40 min-h-[40px] flex-1 resize-none bg-transparent px-2 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground"
1221
+ placeholder="Send a message..."
1222
+ className="max-h-40 min-h-[44px] w-full resize-none bg-transparent px-2 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground"
1154
1223
  />
1224
+ </div>
1225
+ <div className="mt-1 flex items-center justify-between gap-2">
1226
+ <span className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs font-medium text-muted-foreground">
1227
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1228
+ <rect x="4" y="4" width="16" height="16" rx="2" />
1229
+ <rect x="9" y="9" width="6" height="6" />
1230
+ <path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" />
1231
+ </svg>
1232
+ {MODEL}
1233
+ </span>
1155
1234
  <button
1156
1235
  type="button"
1157
1236
  onClick={() => send()}
1158
1237
  disabled={busy || !input.trim() || !address || !network}
1159
- className="shrink-0 rounded-xl bg-gradient-primary px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
1238
+ className="flex size-9 shrink-0 items-center justify-center rounded-full bg-gradient-primary text-white transition hover:brightness-110 disabled:cursor-not-allowed disabled:bg-none disabled:bg-muted disabled:text-muted-foreground"
1239
+ aria-label={busy ? "Working" : "Send"}
1160
1240
  >
1161
- {busy ? (busyStage || "Sending...") : "Send"}
1241
+ {busy ? (
1242
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2" /></svg>
1243
+ ) : (
1244
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5M5 12l7-7 7 7" /></svg>
1245
+ )}
1162
1246
  </button>
1163
1247
  </div>
1164
1248
  {err && (
@@ -1825,7 +1909,8 @@ export function addChatWeb3(opts = {}) {
1825
1909
  written.push(writeFile(path.join(cwd, "app/chat-web3/page.tsx"), NEXTJS_CHAT_WEB3_PAGE, force));
1826
1910
  return {
1827
1911
  written,
1828
- install: `npm install lightnode-sdk viem` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
1912
+ // streamdown renders the assistant answers as markdown (bold, lists, code).
1913
+ install: `npm install lightnode-sdk viem streamdown` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
1829
1914
  template,
1830
1915
  network,
1831
1916
  needsWagmi: !hasWagmi,
@@ -1982,9 +2067,13 @@ export function ConnectButton() {
1982
2067
  type="button"
1983
2068
  onClick={() => connect({ connector })}
1984
2069
  disabled={isPending}
1985
- style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer" }}
2070
+ className="bg-gradient-btn inline-flex items-center gap-2 rounded-[10px] px-5 py-2.5 text-sm font-medium tracking-wide text-white transition hover:brightness-110 disabled:opacity-60"
1986
2071
  >
1987
- {isPending ? "Connecting..." : "Connect wallet"}
2072
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
2073
+ <path d="M19 7V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" />
2074
+ <path d="M21 12h-6a2 2 0 0 0 0 4h6v-4Z" />
2075
+ </svg>
2076
+ {isPending ? "Connecting..." : "Connect Wallet"}
1988
2077
  </button>
1989
2078
  );
1990
2079
  }
@@ -1995,7 +2084,7 @@ export function ConnectButton() {
1995
2084
  type="button"
1996
2085
  onClick={() => switchChain({ chainId: 9200 })}
1997
2086
  disabled={switching}
1998
- style={{ padding: "8px 16px", borderRadius: 8, background: "#fee", cursor: "pointer" }}
2087
+ className="rounded-[10px] border border-destructive/40 bg-destructive/10 px-4 py-2 text-sm font-medium text-destructive transition hover:bg-destructive/20 disabled:opacity-60"
1999
2088
  >
2000
2089
  {switching ? "Switching..." : "Switch to LightChain"}
2001
2090
  </button>
@@ -2006,9 +2095,12 @@ export function ConnectButton() {
2006
2095
  <button
2007
2096
  type="button"
2008
2097
  onClick={() => disconnect()}
2009
- style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer", fontFamily: "monospace" }}
2098
+ className="group inline-flex items-center gap-2 rounded-[10px] border border-border bg-card px-4 py-2 font-mono text-sm text-foreground transition hover:border-primary"
2099
+ title={\`\${chain?.name} - click to disconnect\`}
2010
2100
  >
2011
- {address ? shortAddress(address) : "(unknown)"} ({chain?.name}) - disconnect
2101
+ <span className="size-2 rounded-full bg-success" />
2102
+ {address ? shortAddress(address) : "(unknown)"}
2103
+ <span className="text-muted-foreground group-hover:text-foreground">disconnect</span>
2012
2104
  </button>
2013
2105
  );
2014
2106
  }
@@ -2106,6 +2198,10 @@ export function patchLayoutWithProviders(cwd = process.cwd()) {
2106
2198
  // installs default to the real look instead of the create-next-app starter.
2107
2199
  export const SCAFFOLD_GLOBALS_CSS = `@import "tailwindcss";
2108
2200
 
2201
+ /* let Tailwind v4 see streamdown's classes so markdown answers are styled
2202
+ (harmless when streamdown isn't installed - the path just matches nothing) */
2203
+ @source "../node_modules/streamdown/dist/index.js";
2204
+
2109
2205
  /* dark mode via .dark class (we default the app to dark) */
2110
2206
  @custom-variant dark (&:is(.dark, .dark *));
2111
2207
 
@@ -2304,6 +2400,18 @@ body::before {
2304
2400
  .bg-gradient-primary {
2305
2401
  background-image: var(--color-gradient-primary);
2306
2402
  }
2403
+ /* the lcai-chat connect-button gradient (pink -> purple) */
2404
+ .bg-gradient-btn {
2405
+ background-image: linear-gradient(94deg, #dd00ac 10.66%, #7130c3 53.03%, #410093 96.34%);
2406
+ background-size: 200% auto;
2407
+ }
2408
+ @keyframes pulse-dot {
2409
+ 0%, 100% { opacity: 1; }
2410
+ 50% { opacity: 0.4; }
2411
+ }
2412
+ .animate-pulse-dot {
2413
+ animation: pulse-dot 1.6s ease-in-out infinite;
2414
+ }
2307
2415
  .text-gradient {
2308
2416
  background: linear-gradient(94deg, #dd00ac 10%, #7130c3 53%, #7064e9 96%);
2309
2417
  -webkit-background-clip: text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",