lightnode-sdk 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/add.js +74 -14
  2. package/package.json +1 -1
package/dist/add.js CHANGED
@@ -963,7 +963,7 @@ const NEXTJS_CHAT_WEB3_PAGE = `// app/chat-web3/page.tsx
963
963
 
964
964
  import { useEffect, useRef, useState } from "react";
965
965
  import { useAccount, useWalletClient, usePublicClient } from "wagmi";
966
- import { siweSignIn, GatewayClient, LightChatSession, estimateJobFee, NETWORKS } from "lightnode-sdk";
966
+ import { siweSignIn, GatewayClient, LightChatSession, estimateJobFee, modelId, NETWORKS } from "lightnode-sdk";
967
967
  import { Streamdown } from "streamdown";
968
968
  import { ConnectButton } from "@/components/connect-button";
969
969
  import { LcaiMark } from "@/components/lcai-mark";
@@ -976,6 +976,7 @@ type Turn = {
976
976
  jobId?: string | null;
977
977
  submitTx?: \`0x\${string}\` | null;
978
978
  jobCompletedTx?: \`0x\${string}\` | null;
979
+ sources?: { position: number; title: string; url: string; description: string }[];
979
980
  };
980
981
 
981
982
  // Models live on LightChain mainnet. The visitor picks one per the dropdown.
@@ -1000,6 +1001,10 @@ export default function ChatWeb3() {
1000
1001
  // Reused across turns so follow-ups skip SIWE + createSession.
1001
1002
  const sessionRef = useRef<LightChatSession | null>(null);
1002
1003
  const sessionKeyRef = useRef<string>("");
1004
+ const [searchEnabled, setSearchEnabled] = useState(false);
1005
+ const [searchCapable, setSearchCapable] = useState(false);
1006
+ const searchEnabledRef = useRef(false);
1007
+ searchEnabledRef.current = searchEnabled && searchCapable;
1003
1008
 
1004
1009
  // Read the on-chain fee for the connected network so we can show the
1005
1010
  // visitor the real cost per turn before they click Send.
@@ -1013,6 +1018,23 @@ export default function ChatWeb3() {
1013
1018
  return () => { cancelled = true; };
1014
1019
  }, [network, model]);
1015
1020
 
1021
+ // Gate the Web Search toggle on the model advertising the "search" capability.
1022
+ // (On networks where the capabilities endpoint isn't deployed this 404s and the
1023
+ // toggle stays locked - the honest state until a search-capable worker is up.)
1024
+ useEffect(() => {
1025
+ let cancelled = false;
1026
+ (async () => {
1027
+ try {
1028
+ const gw = new GatewayClient({ network: network ?? "mainnet" });
1029
+ const caps = await gw.getModelCapabilities(modelId(model));
1030
+ if (!cancelled) setSearchCapable(Array.isArray(caps?.capabilities) && caps.capabilities.includes("search"));
1031
+ } catch {
1032
+ if (!cancelled) setSearchCapable(false);
1033
+ }
1034
+ })();
1035
+ return () => { cancelled = true; };
1036
+ }, [network, model]);
1037
+
1016
1038
  // Keep the latest turn in view. Instant while streaming (smooth scrolling on
1017
1039
  // every chunk competes for the main thread); smooth once idle.
1018
1040
  useEffect(() => {
@@ -1046,7 +1068,9 @@ export default function ChatWeb3() {
1046
1068
  */
1047
1069
  async function ensureSession(): Promise<LightChatSession> {
1048
1070
  if (!walletClient || !publicClient || !address || !network) throw new Error("connect a wallet first");
1049
- const key = \`\${address}:\${network}:\${model}\`;
1071
+ // A search session must bind to a search-capable worker, so it keys separately.
1072
+ const wantSearch = searchEnabledRef.current;
1073
+ const key = \`\${address}:\${network}:\${model}:\${wantSearch}\`;
1050
1074
  const existing = sessionRef.current;
1051
1075
  if (existing && !existing.expired && sessionKeyRef.current === key) return existing;
1052
1076
  setBusyStage("Sign in with your wallet (SIWE)...");
@@ -1059,6 +1083,7 @@ export default function ChatWeb3() {
1059
1083
  publicClient: publicClient as unknown as Parameters<typeof LightChatSession.open>[0]["publicClient"],
1060
1084
  network: NETWORKS[network],
1061
1085
  model,
1086
+ ...(wantSearch ? { requiredCapabilities: ["search"] } : {}),
1062
1087
  });
1063
1088
  sessionRef.current = chat;
1064
1089
  sessionKeyRef.current = key;
@@ -1086,7 +1111,7 @@ export default function ChatWeb3() {
1086
1111
  patchLastAssistant({ text: totalSoFar });
1087
1112
  };
1088
1113
  const onStage = (s: string) => setBusyStage(s);
1089
- const sendOpts = { onChunk, onStage };
1114
+ const sendOpts = { onChunk, onStage, searchEnabled: searchEnabledRef.current };
1090
1115
 
1091
1116
  const chat = await ensureSession();
1092
1117
  const result = await chat.send(prompt, sendOpts).catch(async () => {
@@ -1105,6 +1130,7 @@ export default function ChatWeb3() {
1105
1130
  jobId: result.jobId?.toString() ?? null,
1106
1131
  submitTx: result.txs?.submitJob ?? null,
1107
1132
  jobCompletedTx: result.txs?.jobCompleted ?? null,
1133
+ sources: result.sources,
1108
1134
  });
1109
1135
  } catch (e) {
1110
1136
  // Roll back the optimistic user bubble so the visitor can retry.
@@ -1184,6 +1210,22 @@ export default function ChatWeb3() {
1184
1210
  {busyStage || "Thinking..."}
1185
1211
  </div>
1186
1212
  )}
1213
+ {t.sources && t.sources.length > 0 && (
1214
+ <div className="mt-1 border-t border-border pt-3">
1215
+ <div className="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">Sources</div>
1216
+ <div className="grid gap-2">
1217
+ {t.sources.map((s) => (
1218
+ <a key={s.position + "-" + s.url} href={s.url} target="_blank" rel="noopener noreferrer" className="grid grid-cols-[1.5rem_1fr] gap-2 rounded-lg bg-surface-base-faint px-2.5 py-2 transition-colors hover:bg-card">
1219
+ <span className="flex size-5 items-center justify-center rounded-md bg-card text-[11px] font-medium text-muted-foreground">{s.position}</span>
1220
+ <span className="min-w-0">
1221
+ <span className="block truncate text-sm font-medium text-foreground hover:underline">{s.title || s.url}</span>
1222
+ <span className="block truncate text-xs text-muted-foreground">{s.description || s.url}</span>
1223
+ </span>
1224
+ </a>
1225
+ ))}
1226
+ </div>
1227
+ </div>
1228
+ )}
1187
1229
  {t.submitTx && (
1188
1230
  <div className="flex flex-wrap items-center gap-3 text-[11px] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100">
1189
1231
  <button
@@ -1233,17 +1275,35 @@ export default function ChatWeb3() {
1233
1275
  />
1234
1276
  </div>
1235
1277
  <div className="mt-1 flex items-center justify-between gap-2">
1236
- <select
1237
- value={model}
1238
- onChange={(e) => setModel(e.target.value as ModelId)}
1239
- disabled={busy}
1240
- title="Model (both live on LightChain mainnet)"
1241
- 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"
1242
- >
1243
- {MODELS.map((m) => (
1244
- <option key={m} value={m}>{m}</option>
1245
- ))}
1246
- </select>
1278
+ <div className="flex items-center gap-3">
1279
+ <select
1280
+ value={model}
1281
+ onChange={(e) => setModel(e.target.value as ModelId)}
1282
+ disabled={busy}
1283
+ title="Model (both live on LightChain mainnet)"
1284
+ 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"
1285
+ >
1286
+ {MODELS.map((m) => (
1287
+ <option key={m} value={m}>{m}</option>
1288
+ ))}
1289
+ </select>
1290
+ <div
1291
+ className="flex items-center gap-2"
1292
+ title={searchCapable ? "Let the worker search the web for this turn" : "No web-search-capable worker is online for this model right now."}
1293
+ >
1294
+ <button
1295
+ type="button"
1296
+ role="switch"
1297
+ aria-checked={searchEnabled && searchCapable}
1298
+ disabled={!searchCapable || busy}
1299
+ onClick={() => setSearchEnabled((v) => !v)}
1300
+ className={"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-50 " + (searchEnabled && searchCapable ? "bg-primary" : "bg-muted-foreground/30")}
1301
+ >
1302
+ <span className={"inline-block size-4 rounded-full bg-white shadow transition-transform " + (searchEnabled && searchCapable ? "translate-x-4" : "translate-x-0.5")} />
1303
+ </button>
1304
+ <span className="text-xs text-muted-foreground">Web Search</span>
1305
+ </div>
1306
+ </div>
1247
1307
  <button
1248
1308
  type="button"
1249
1309
  onClick={() => send()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
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",