← 開發者指南

React SPA 整合

透過一個小型自訂 hook,把 Quravin SDK 接進 React 元件 — 單一客戶端實例、真正的 loading/error 狀態,不會在每次 render 時重新建立實例。

SDK 沒有專屬於 React 的建置版本 — 用的是任何純網頁都會用的同一個 CDN 客戶端。在 React app 中改變的,是客戶端實例存放在哪裡,以及如何把 loading/error 狀態暴露給元件。本篇涵蓋 這兩點。

1. 載入 SDK

import { Quravin } from "@quravin/sdk" 這種寫法目前還沒上公開的 npm registry — 如果您需要這條 路徑,請洽詢您的平台維運方。現階段,請用 純網頁整合 相同的方式載入:經典的 <script src> 標籤在打包過的 SPA 裡一樣能用 — 把它加進 index.html,再從 window.Quravin.Quravin 讀取客戶端 (這個重複命名確實有點彆扭;未來版本的 SDK 會改善這點)。

<!-- index.html -->
<script src="https://js.quravin.com/v1.js"></script>

2. 從您自己的 backend 取得 session token

規則與任何瀏覽器程式碼相同:絕對不要把靜態的 x-api-key 放進 React app — 任何人都能從 打包後的程式碼中讀到它。您的 backend 會發行一把短效、每位使用者一個的 session JWT 交給頁面。 如果您還沒有 token 發行 endpoint,Node.js 伺服器整合 展示了如何建置一個(不論您 backend 用什麼語言,模式都相同 — 只有 endpoint 的實作方式不同)。

async function fetchSessionToken() {
  const r = await fetch("/api/ai-token", { credentials: "include" });
  if (!r.ok) throw new Error("Token fetch failed");
  return (await r.json()).token;
}

3. 只建立一次客戶端 — 不要在每次 render 時建立

一個常見的錯誤:在元件本體中直接 new window.Quravin.Quravin(...) 會在每次 render 時建立新的客戶端, 這會讓任何進行中的 onTokenExpired 重試邏輯失效。請使用 ref 或 module scope,只建立一次:

import { useRef } from "react";

// window.Quravin is the CDN script's global (see Step 1) — there's no npm type
// import yet, so declare just enough shape for what this hook uses.
type QuravinClient = { run(args: { pipeline: string; inputs: Record<string, unknown> }): Promise<unknown> };
declare global {
  interface Window {
    Quravin: {
      Quravin: new (opts: {
        endpoint: string;
        sessionToken: string;
        onTokenExpired: () => Promise<string>;
      }) => QuravinClient;
    };
  }
}

function useQuravinClient() {
  const clientRef = useRef<QuravinClient | null>(null);
  if (!clientRef.current) {
    clientRef.current = new window.Quravin.Quravin({
      endpoint: import.meta.env.VITE_QURAVIN_API_BASE, // your bundler's env-var convention
      sessionToken: "", // placeholder — set for real on first fetchSessionToken() below
      onTokenExpired: fetchSessionToken,
    });
  }
  return clientRef.current;
}

實務上,多數 app 會在掛載時(或登入時)就取得一次 token,而不是等到第一次呼叫時才延遲取得 — 下方完整的 hook 就是這種做法。

4. 一個掌管執行狀態的小型 hook

元件不應該各自重新實作 loading/error/result 狀態。用同一個 hook,到處重複使用:

import { useCallback, useRef, useState } from "react";

type RunState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "error"; error: string }
  | { status: "done"; result: T };

export function useQuravin() {
  const clientRef = useRef<QuravinClient | null>(null);
  const getClient = useCallback(() => {
    if (!clientRef.current) {
      clientRef.current = new window.Quravin.Quravin({
        endpoint: import.meta.env.VITE_QURAVIN_API_BASE,
        sessionToken: "",
        onTokenExpired: fetchSessionToken,
      });
    }
    return clientRef.current;
  }, []);

  const [state, setState] = useState<RunState<unknown>>({ status: "idle" });

  const run = useCallback(
    async (pipeline: string, inputs: Record<string, unknown>) => {
      setState({ status: "loading" });
      try {
        const result = await getClient().run({ pipeline, inputs });
        setState({ status: "done", result });
        return result;
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        setState({ status: "error", error: message });
        throw err;
      }
    },
    [getClient],
  );

  return { state, run };
}

getClient() 目前仍以空字串的 sessionToken 延遲建立客戶端 — 一旦您接好自己的 /api/ai-token 路由,就可以換成真正的「掛載時發行」流程(在 useEffect 中取得一次 token 並儲存);上面的寫法 刻意保持精簡,讓您能清楚看到該把它接在哪裡。

5. 在元件中使用它

function TranslateBox() {
  const { state, run } = useQuravin();
  const [text, setText] = useState("");

  const onTranslate = () =>
    run("translate-string", { text, target_language: "de" }).catch(() => {
      /* state.error already holds the message */
    });

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={onTranslate} disabled={state.status === "loading"}>
        Translate
      </button>
      {state.status === "error" && <p role="alert">{state.error}</p>}
      {state.status === "done" && <p>{(state.result as { translation: string }).translation}</p>}
    </div>
  );
}

批次處理多筆輸入

若要對許多筆輸入執行同一個 pipeline(例如翻譯表格中的每一列),請使用 ai.runMany(...), 而不是逐一呼叫 ai.run() — 它會為您處理有限並發數與逐筆進度回報。SDK 更完整的 Integration Guide(請洽詢您的平台維運方索取 — 它並非公開文件)記載了完整的選項格式 (concurrencyerrorStrategyonProgress)與每個 pipeline 的結果格式對照表。

錯誤處理

ai.run()FAILEDCANCELED ticket 以及 HTTP 錯誤發生時都會 reject — 上面的 hook 已經把 兩者都導向 state.error。完整的狀態碼清單,以及每個狀態碼對已認證路徑代表的意義,請見 工具參考手冊 的 Errors 章節。