lightnode-sdk 0.8.7 → 0.8.9

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.
Files changed (2) hide show
  1. package/dist/add.js +81 -35
  2. package/package.json +1 -1
package/dist/add.js CHANGED
@@ -978,7 +978,9 @@ type Turn = {
978
978
  jobCompletedTx?: \`0x\${string}\` | null;
979
979
  };
980
980
 
981
- const MODEL = "llama3-8b";
981
+ // Models live on LightChain mainnet. The visitor picks one per the dropdown.
982
+ const MODELS = ["llama3-8b", "llama3-70b"] as const;
983
+ type ModelId = (typeof MODELS)[number];
982
984
 
983
985
  export default function ChatWeb3() {
984
986
  const { address, chain } = useAccount();
@@ -987,6 +989,7 @@ export default function ChatWeb3() {
987
989
  const { data: walletClient } = useWalletClient({ chainId: chain?.id });
988
990
  const publicClient = usePublicClient({ chainId: chain?.id });
989
991
 
992
+ const [model, setModel] = useState<ModelId>("llama3-8b");
990
993
  const [turns, setTurns] = useState<Turn[]>([]);
991
994
  const [input, setInput] = useState("");
992
995
  const [busy, setBusy] = useState(false);
@@ -1000,16 +1003,17 @@ export default function ChatWeb3() {
1000
1003
  useEffect(() => {
1001
1004
  if (!network) { setFeeLcai(null); return; }
1002
1005
  let cancelled = false;
1003
- estimateJobFee(NETWORKS[network], MODEL).then(
1006
+ estimateJobFee(NETWORKS[network], model).then(
1004
1007
  (fee) => { if (!cancelled) setFeeLcai(fee); },
1005
1008
  () => { if (!cancelled) setFeeLcai(null); },
1006
1009
  );
1007
1010
  return () => { cancelled = true; };
1008
- }, [network]);
1011
+ }, [network, model]);
1009
1012
 
1010
- // Keep the latest turn (and the "writing on chain" indicator) in view.
1013
+ // Keep the latest turn in view. Instant while streaming (smooth scrolling on
1014
+ // every chunk competes for the main thread); smooth once idle.
1011
1015
  useEffect(() => {
1012
- endRef.current?.scrollIntoView({ behavior: "smooth" });
1016
+ endRef.current?.scrollIntoView({ behavior: busy ? "auto" : "smooth", block: "nearest" });
1013
1017
  }, [turns, busy]);
1014
1018
 
1015
1019
  /** Build a single prompt from history + new user input. */
@@ -1061,7 +1065,7 @@ export default function ChatWeb3() {
1061
1065
  wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1062
1066
  publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1063
1067
  network: NETWORKS[network],
1064
- model: MODEL,
1068
+ model,
1065
1069
  jobCompletedTimeoutMs: 120_000,
1066
1070
  maxRetries: 1,
1067
1071
  // Stream each decrypted chunk into the assistant bubble as it arrives.
@@ -1143,9 +1147,15 @@ export default function ChatWeb3() {
1143
1147
  <LcaiMark className="mt-0.5 size-7 shrink-0" />
1144
1148
  <div className="flex min-w-0 flex-1 flex-col gap-2">
1145
1149
  {t.text ? (
1146
- <div className="max-w-none text-sm leading-relaxed text-foreground [&_*:first-child]:mt-0 [&_*:last-child]:mb-0">
1147
- <Streamdown>{t.text}</Streamdown>
1148
- </div>
1150
+ t.streaming ? (
1151
+ // While streaming, render plain text (cheap) - markdown is
1152
+ // parsed once when the turn finalizes, below.
1153
+ <div className="whitespace-pre-wrap break-words text-sm leading-relaxed text-foreground">{t.text}</div>
1154
+ ) : (
1155
+ <div className="max-w-none text-sm leading-relaxed text-foreground [&_*:first-child]:mt-0 [&_*:last-child]:mb-0">
1156
+ <Streamdown>{t.text}</Streamdown>
1157
+ </div>
1158
+ )
1149
1159
  ) : (
1150
1160
  <div className="animate-pulse-dot pt-1 text-sm text-muted-foreground">
1151
1161
  {busyStage || "Writing on chain..."}
@@ -1200,14 +1210,17 @@ export default function ChatWeb3() {
1200
1210
  />
1201
1211
  </div>
1202
1212
  <div className="mt-1 flex items-center justify-between gap-2">
1203
- <span className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs font-medium text-muted-foreground">
1204
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1205
- <rect x="4" y="4" width="16" height="16" rx="2" />
1206
- <rect x="9" y="9" width="6" height="6" />
1207
- <path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" />
1208
- </svg>
1209
- {MODEL}
1210
- </span>
1213
+ <select
1214
+ value={model}
1215
+ onChange={(e) => setModel(e.target.value as ModelId)}
1216
+ disabled={busy}
1217
+ title="Model (both live on LightChain mainnet)"
1218
+ className="rounded-lg border border-border bg-background px-2 py-1 text-xs font-medium text-muted-foreground outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
1219
+ >
1220
+ {MODELS.map((m) => (
1221
+ <option key={m} value={m}>{m}</option>
1222
+ ))}
1223
+ </select>
1211
1224
  <button
1212
1225
  type="button"
1213
1226
  onClick={() => send()}
@@ -1260,7 +1273,8 @@ type Result = {
1260
1273
  elapsedMs: number;
1261
1274
  };
1262
1275
 
1263
- const MODEL = "llama3-8b";
1276
+ const MODELS = ["llama3-8b", "llama3-70b"] as const;
1277
+ type ModelId = (typeof MODELS)[number];
1264
1278
 
1265
1279
  export default function InferenceWeb3() {
1266
1280
  const { address, chain } = useAccount();
@@ -1269,6 +1283,7 @@ export default function InferenceWeb3() {
1269
1283
  const { data: walletClient } = useWalletClient({ chainId: chain?.id });
1270
1284
  const publicClient = usePublicClient({ chainId: chain?.id });
1271
1285
 
1286
+ const [model, setModel] = useState<ModelId>("llama3-8b");
1272
1287
  const [system, setSystem] = useState("You are a concise assistant. Reply in one or two short sentences.");
1273
1288
  const [prompt, setPrompt] = useState("Reply with the single word OK.");
1274
1289
  const [busy, setBusy] = useState(false);
@@ -1281,12 +1296,12 @@ export default function InferenceWeb3() {
1281
1296
  useEffect(() => {
1282
1297
  if (!network) { setFeeLcai(null); return; }
1283
1298
  let cancelled = false;
1284
- estimateJobFee(NETWORKS[network], MODEL).then(
1299
+ estimateJobFee(NETWORKS[network], model).then(
1285
1300
  (fee) => { if (!cancelled) setFeeLcai(fee); },
1286
1301
  () => { if (!cancelled) setFeeLcai(null); },
1287
1302
  );
1288
1303
  return () => { cancelled = true; };
1289
- }, [network]);
1304
+ }, [network, model]);
1290
1305
 
1291
1306
  async function run() {
1292
1307
  if (!walletClient || !publicClient || !address || !network) {
@@ -1312,7 +1327,7 @@ export default function InferenceWeb3() {
1312
1327
  wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1313
1328
  publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1314
1329
  network: NETWORKS[network],
1315
- model: MODEL,
1330
+ model,
1316
1331
  jobCompletedTimeoutMs: 120_000,
1317
1332
  maxRetries: 1,
1318
1333
  // Stream the answer live as decrypted chunks arrive.
@@ -1375,10 +1390,23 @@ export default function InferenceWeb3() {
1375
1390
  className="resize-none rounded-xl border border-border bg-card px-3 py-2 font-mono text-xs text-foreground outline-none focus:ring-2 focus:ring-primary" />
1376
1391
  </label>
1377
1392
 
1378
- <button type="button" onClick={() => run()} disabled={busy || !prompt.trim() || !address || !network}
1379
- className="self-start 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">
1380
- {busy ? (busyStage || "Running...") : "Run inference"}
1381
- </button>
1393
+ <div className="flex items-center gap-3">
1394
+ <select
1395
+ value={model}
1396
+ onChange={(e) => setModel(e.target.value as ModelId)}
1397
+ disabled={busy}
1398
+ title="Model (both live on LightChain mainnet)"
1399
+ className="rounded-xl border border-border bg-card px-2 py-2 text-xs font-medium text-muted-foreground outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
1400
+ >
1401
+ {MODELS.map((m) => (
1402
+ <option key={m} value={m}>{m}</option>
1403
+ ))}
1404
+ </select>
1405
+ <button type="button" onClick={() => run()} disabled={busy || !prompt.trim() || !address || !network}
1406
+ className="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">
1407
+ {busy ? (busyStage || "Running...") : "Run inference"}
1408
+ </button>
1409
+ </div>
1382
1410
 
1383
1411
  {err && (
1384
1412
  <p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
@@ -1391,10 +1419,13 @@ export default function InferenceWeb3() {
1391
1419
  <div className="flex gap-3">
1392
1420
  <LcaiMark className="mt-0.5 size-7 shrink-0" />
1393
1421
  <div className="flex min-w-0 flex-1 flex-col gap-2">
1394
- {result || stream ? (
1422
+ {result ? (
1395
1423
  <div className="max-w-none text-sm leading-relaxed text-foreground [&_*:first-child]:mt-0 [&_*:last-child]:mb-0">
1396
- <Streamdown>{result ? result.answer : stream}</Streamdown>
1424
+ <Streamdown>{result.answer}</Streamdown>
1397
1425
  </div>
1426
+ ) : stream ? (
1427
+ // Plain text while streaming; markdown is parsed once on completion.
1428
+ <div className="whitespace-pre-wrap break-words text-sm leading-relaxed text-foreground">{stream}</div>
1398
1429
  ) : (
1399
1430
  <div className="animate-pulse-dot pt-1 text-sm text-muted-foreground">
1400
1431
  {busyStage || "Writing on chain..."}
@@ -1454,7 +1485,8 @@ type Result = {
1454
1485
  jobCompleted: \`0x\${string}\` | null;
1455
1486
  };
1456
1487
 
1457
- const MODEL = "llama3-8b";
1488
+ const MODELS = ["llama3-8b", "llama3-70b"] as const;
1489
+ type ModelId = (typeof MODELS)[number];
1458
1490
 
1459
1491
  export default function JudgeWeb3() {
1460
1492
  const { address, chain } = useAccount();
@@ -1463,6 +1495,7 @@ export default function JudgeWeb3() {
1463
1495
  const { data: walletClient } = useWalletClient({ chainId: chain?.id });
1464
1496
  const publicClient = usePublicClient({ chainId: chain?.id });
1465
1497
 
1498
+ const [model, setModel] = useState<ModelId>("llama3-8b");
1466
1499
  const [criteria, setCriteria] = useState("Run a mile under 8 minutes");
1467
1500
  const [evidence, setEvidence] = useState('{"distance_km": 1.61, "time_minutes": 7.4}');
1468
1501
  const [busy, setBusy] = useState(false);
@@ -1475,12 +1508,12 @@ export default function JudgeWeb3() {
1475
1508
  useEffect(() => {
1476
1509
  if (!network) { setFeeLcai(null); return; }
1477
1510
  let cancelled = false;
1478
- estimateJobFee(NETWORKS[network], MODEL).then(
1511
+ estimateJobFee(NETWORKS[network], model).then(
1479
1512
  (fee) => { if (!cancelled) setFeeLcai(fee); },
1480
1513
  () => { if (!cancelled) setFeeLcai(null); },
1481
1514
  );
1482
1515
  return () => { cancelled = true; };
1483
- }, [network]);
1516
+ }, [network, model]);
1484
1517
 
1485
1518
  /** Parse the verdict defensively; fall back to the first {...} block. */
1486
1519
  function parseVerdict(answer: string): Verdict | null {
@@ -1525,7 +1558,7 @@ Reply with STRICT JSON only, matching: { "passed": boolean, "confidence": 0-1, "
1525
1558
  wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1526
1559
  publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1527
1560
  network: NETWORKS[network],
1528
- model: MODEL,
1561
+ model,
1529
1562
  jobCompletedTimeoutMs: 120_000,
1530
1563
  maxRetries: 1,
1531
1564
  // Show the model's raw output streaming in while it generates the verdict.
@@ -1588,10 +1621,23 @@ Reply with STRICT JSON only, matching: { "passed": boolean, "confidence": 0-1, "
1588
1621
  className="resize-none rounded-xl border border-border bg-card px-3 py-2 font-mono text-xs text-foreground outline-none focus:ring-2 focus:ring-primary" />
1589
1622
  </label>
1590
1623
 
1591
- <button type="button" onClick={() => run()} disabled={busy || !criteria.trim() || !evidence.trim() || !address || !network}
1592
- className="self-start 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">
1593
- {busy ? (busyStage || "Judging...") : "Get AI verdict"}
1594
- </button>
1624
+ <div className="flex items-center gap-3">
1625
+ <select
1626
+ value={model}
1627
+ onChange={(e) => setModel(e.target.value as ModelId)}
1628
+ disabled={busy}
1629
+ title="Model (both live on LightChain mainnet)"
1630
+ className="rounded-xl border border-border bg-card px-2 py-2 text-xs font-medium text-muted-foreground outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
1631
+ >
1632
+ {MODELS.map((m) => (
1633
+ <option key={m} value={m}>{m}</option>
1634
+ ))}
1635
+ </select>
1636
+ <button type="button" onClick={() => run()} disabled={busy || !criteria.trim() || !evidence.trim() || !address || !network}
1637
+ className="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">
1638
+ {busy ? (busyStage || "Judging...") : "Get AI verdict"}
1639
+ </button>
1640
+ </div>
1595
1641
 
1596
1642
  {err && (
1597
1643
  <p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
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",