lightnode-sdk 0.8.3 → 0.8.4

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,9 +961,10 @@ 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 = {
@@ -990,6 +991,7 @@ export default function ChatWeb3() {
990
991
  const [busyStage, setBusyStage] = useState("");
991
992
  const [err, setErr] = useState<string | null>(null);
992
993
  const [feeLcai, setFeeLcai] = useState<number | null>(null);
994
+ const endRef = useRef<HTMLDivElement>(null);
993
995
 
994
996
  // Read the on-chain fee for the connected network so we can show the
995
997
  // visitor the real cost per turn before they click Send.
@@ -1003,6 +1005,11 @@ export default function ChatWeb3() {
1003
1005
  return () => { cancelled = true; };
1004
1006
  }, [network]);
1005
1007
 
1008
+ // Keep the latest turn (and the "writing on chain" indicator) in view.
1009
+ useEffect(() => {
1010
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
1011
+ }, [turns, busy]);
1012
+
1006
1013
  /** Build a single prompt from history + new user input. */
1007
1014
  function composePrompt(history: Turn[], next: string, system: string): string {
1008
1015
  const lines: string[] = [];
@@ -1074,10 +1081,10 @@ export default function ChatWeb3() {
1074
1081
  }
1075
1082
 
1076
1083
  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">
1084
+ <main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col px-4 py-6">
1085
+ <header className="flex items-center justify-between gap-3 pb-6">
1079
1086
  <div className="min-w-0">
1080
- <h1 className="text-base font-semibold text-foreground">Chat</h1>
1087
+ <h1 className="font-semibold text-foreground">Chat</h1>
1081
1088
  <p className="truncate text-xs text-muted-foreground">
1082
1089
  {network ? (
1083
1090
  <>Signed from your wallet on {network} · {feeLcai != null ? feeLcai + " LCAI" : "..."}/turn + gas</>
@@ -1089,10 +1096,12 @@ export default function ChatWeb3() {
1089
1096
  <ConnectButton />
1090
1097
  </header>
1091
1098
 
1092
- <div className="flex flex-1 flex-col gap-3 overflow-y-auto py-6">
1093
- {turns.length === 0 ? (
1099
+ <div className="flex flex-1 flex-col gap-6 overflow-y-auto pb-6">
1100
+ {turns.length === 0 && !busy ? (
1094
1101
  <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>
1102
+ <h2 className="text-3xl font-medium tracking-tight text-foreground">
1103
+ Start talking with <span className="text-gradient">AI Chat</span>
1104
+ </h2>
1096
1105
  <p className="max-w-sm text-sm text-muted-foreground">
1097
1106
  Connect your wallet and send a message. Each turn is signed and paid from your
1098
1107
  own wallet, no backend required.
@@ -1104,61 +1113,88 @@ export default function ChatWeb3() {
1104
1113
  )}
1105
1114
  </div>
1106
1115
  ) : (
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>
1124
- )}
1125
- {t.jobId && <span>job #{t.jobId}</span>}
1126
- {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>
1135
- )}
1116
+ <>
1117
+ {turns.map((t, i) =>
1118
+ t.role === "user" ? (
1119
+ <div key={i} className="flex justify-end">
1120
+ <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">
1121
+ {t.text}
1122
+ </div>
1136
1123
  </div>
1137
- ) : null}
1138
- </div>
1139
- ))
1124
+ ) : (
1125
+ <div key={i} className="group flex flex-col gap-2">
1126
+ <div className="prose-sm max-w-none text-sm leading-relaxed text-foreground [&_*:first-child]:mt-0 [&_*:last-child]:mb-0">
1127
+ <Streamdown>{t.text}</Streamdown>
1128
+ </div>
1129
+ <div className="flex flex-wrap items-center gap-3 text-[11px] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100">
1130
+ <button
1131
+ type="button"
1132
+ onClick={() => navigator.clipboard?.writeText(t.text)}
1133
+ className="inline-flex items-center gap-1 hover:text-foreground"
1134
+ aria-label="Copy"
1135
+ >
1136
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1137
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
1138
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
1139
+ </svg>
1140
+ Copy
1141
+ </button>
1142
+ {t.worker && (
1143
+ <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/address/\${t.worker}\`} target="_blank" rel="noopener noreferrer">worker</a>
1144
+ )}
1145
+ {t.jobId && <span>job #{t.jobId}</span>}
1146
+ {t.submitTx && (
1147
+ <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/tx/\${t.submitTx}\`} target="_blank" rel="noopener noreferrer">submitJob</a>
1148
+ )}
1149
+ {t.jobCompletedTx && (
1150
+ <a className="hover:text-foreground hover:underline" href={\`https://\${network}.lightscan.app/tx/\${t.jobCompletedTx}\`} target="_blank" rel="noopener noreferrer">completed</a>
1151
+ )}
1152
+ </div>
1153
+ </div>
1154
+ )
1155
+ )}
1156
+ {busy && (
1157
+ <div className="animate-pulse-dot text-sm text-muted-foreground">
1158
+ {busyStage || "Writing on chain..."}
1159
+ </div>
1160
+ )}
1161
+ </>
1140
1162
  )}
1163
+ <div ref={endRef} />
1141
1164
  </div>
1142
1165
 
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">
1145
- <textarea
1146
- value={input}
1147
- onChange={(e) => setInput(e.target.value)}
1148
- onKeyDown={(e) => {
1149
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (!busy && input.trim()) send(); }
1150
- }}
1151
- 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"
1154
- />
1166
+ <div className="rounded-2xl border border-border bg-card p-3">
1167
+ <textarea
1168
+ value={input}
1169
+ onChange={(e) => setInput(e.target.value)}
1170
+ onKeyDown={(e) => {
1171
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (!busy && input.trim()) send(); }
1172
+ }}
1173
+ rows={1}
1174
+ placeholder="Send a message..."
1175
+ 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"
1176
+ />
1177
+ <div className="mt-1 flex items-center justify-between gap-2">
1178
+ <span className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs font-medium text-muted-foreground">
1179
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1180
+ <rect x="4" y="4" width="16" height="16" rx="2" />
1181
+ <rect x="9" y="9" width="6" height="6" />
1182
+ <path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" />
1183
+ </svg>
1184
+ {MODEL}
1185
+ </span>
1155
1186
  <button
1156
1187
  type="button"
1157
1188
  onClick={() => send()}
1158
1189
  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"
1190
+ 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"
1191
+ aria-label={busy ? "Working" : "Send"}
1160
1192
  >
1161
- {busy ? (busyStage || "Sending...") : "Send"}
1193
+ {busy ? (
1194
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2" /></svg>
1195
+ ) : (
1196
+ <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>
1197
+ )}
1162
1198
  </button>
1163
1199
  </div>
1164
1200
  {err && (
@@ -1825,7 +1861,8 @@ export function addChatWeb3(opts = {}) {
1825
1861
  written.push(writeFile(path.join(cwd, "app/chat-web3/page.tsx"), NEXTJS_CHAT_WEB3_PAGE, force));
1826
1862
  return {
1827
1863
  written,
1828
- install: `npm install lightnode-sdk viem` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
1864
+ // streamdown renders the assistant answers as markdown (bold, lists, code).
1865
+ install: `npm install lightnode-sdk viem streamdown` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
1829
1866
  template,
1830
1867
  network,
1831
1868
  needsWagmi: !hasWagmi,
@@ -1982,9 +2019,13 @@ export function ConnectButton() {
1982
2019
  type="button"
1983
2020
  onClick={() => connect({ connector })}
1984
2021
  disabled={isPending}
1985
- style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer" }}
2022
+ 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
2023
  >
1987
- {isPending ? "Connecting..." : "Connect wallet"}
2024
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
2025
+ <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" />
2026
+ <path d="M21 12h-6a2 2 0 0 0 0 4h6v-4Z" />
2027
+ </svg>
2028
+ {isPending ? "Connecting..." : "Connect Wallet"}
1988
2029
  </button>
1989
2030
  );
1990
2031
  }
@@ -1995,7 +2036,7 @@ export function ConnectButton() {
1995
2036
  type="button"
1996
2037
  onClick={() => switchChain({ chainId: 9200 })}
1997
2038
  disabled={switching}
1998
- style={{ padding: "8px 16px", borderRadius: 8, background: "#fee", cursor: "pointer" }}
2039
+ 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
2040
  >
2000
2041
  {switching ? "Switching..." : "Switch to LightChain"}
2001
2042
  </button>
@@ -2006,9 +2047,12 @@ export function ConnectButton() {
2006
2047
  <button
2007
2048
  type="button"
2008
2049
  onClick={() => disconnect()}
2009
- style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer", fontFamily: "monospace" }}
2050
+ 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"
2051
+ title={\`\${chain?.name} - click to disconnect\`}
2010
2052
  >
2011
- {address ? shortAddress(address) : "(unknown)"} ({chain?.name}) - disconnect
2053
+ <span className="size-2 rounded-full bg-success" />
2054
+ {address ? shortAddress(address) : "(unknown)"}
2055
+ <span className="text-muted-foreground group-hover:text-foreground">disconnect</span>
2012
2056
  </button>
2013
2057
  );
2014
2058
  }
@@ -2106,6 +2150,10 @@ export function patchLayoutWithProviders(cwd = process.cwd()) {
2106
2150
  // installs default to the real look instead of the create-next-app starter.
2107
2151
  export const SCAFFOLD_GLOBALS_CSS = `@import "tailwindcss";
2108
2152
 
2153
+ /* let Tailwind v4 see streamdown's classes so markdown answers are styled
2154
+ (harmless when streamdown isn't installed - the path just matches nothing) */
2155
+ @source "../node_modules/streamdown/dist/index.js";
2156
+
2109
2157
  /* dark mode via .dark class (we default the app to dark) */
2110
2158
  @custom-variant dark (&:is(.dark, .dark *));
2111
2159
 
@@ -2304,6 +2352,18 @@ body::before {
2304
2352
  .bg-gradient-primary {
2305
2353
  background-image: var(--color-gradient-primary);
2306
2354
  }
2355
+ /* the lcai-chat connect-button gradient (pink -> purple) */
2356
+ .bg-gradient-btn {
2357
+ background-image: linear-gradient(94deg, #dd00ac 10.66%, #7130c3 53.03%, #410093 96.34%);
2358
+ background-size: 200% auto;
2359
+ }
2360
+ @keyframes pulse-dot {
2361
+ 0%, 100% { opacity: 1; }
2362
+ 50% { opacity: 0.4; }
2363
+ }
2364
+ .animate-pulse-dot {
2365
+ animation: pulse-dot 1.6s ease-in-out infinite;
2366
+ }
2307
2367
  .text-gradient {
2308
2368
  background: linear-gradient(94deg, #dd00ac 10%, #7130c3 53%, #7064e9 96%);
2309
2369
  -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.4",
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",