← 開発者ガイド

React SPA 統合

小さなカスタムフックで Quravin SDK を React コンポーネントに組み込みます — クライアントインスタンスは 1 つ、実際の loading/error state を持ち、レンダーのたびに再インスタンス化しません。

SDK に React 専用のビルドはありません — プレーンな Web ページが使うのと同じ CDN クライアントです。 React アプリで変わるのは、クライアントインスタンスがどこに存在するかloading/error state をどうコンポーネントに公開するかです。このガイドでは両方を扱います。

1. SDK を読み込む

import { Quravin } from "@quravin/sdk" というエルゴノミクスはまだ公開 npm レジストリには 載っていません — その経路が必要な場合はプラットフォームの運営者にお問い合わせください。 今日のところは、プレーンな Web ページ統合 と同じ方法で読み込みます:おなじみの <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 アプリに絶対に含めないで ください — バンドルから誰でも読み取れてしまいます。あなたの backend が短期の、ユーザーごとの session JWT を発行してページに渡します。トークン発行エンドポイントがまだない場合は、Node.js サーバー統合 にその作り方が示されています (backend の言語が何であってもパターンは同じで、エンドポイントの実装だけが異なります)。

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. クライアントを一度だけ作成する — レンダーのたびに作らない

よくある間違い:コンポーネント本体の中で new window.Quravin.Quravin(...) をインスタンス化すると、 レンダーのたびに新しいクライアントが作られ、進行中の onTokenExpired リトライロジックが 失われます。ref かモジュールスコープを使って一度だけ作成してください。

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;
}

実際には、ほとんどのアプリはトークンを、呼び出しのたびに遅延取得するのではなく、マウント時 (またはログイン時)に一度だけ取得します — それを行うバージョンについては下記の完全なフックを 参照してください。

4. 実行状態を保持する小さなフック

各コンポーネントがそれぞれ loading/error/result の state を再実装すべきではありません。1 つの フックをどこでも再利用します。

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 の中で一度トークンを取得して 保存する)に差し替えてください。上記の形はどこにそれを組み込めばよいかが分かりやすいよう、 意図的に最小限にしてあります。

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>
  );
}

複数の入力をバッチ処理する

同じパイプラインを多数の入力にわたって実行する場合(例えばテーブルの全行を翻訳する場合)は、 入力ごとに ai.run() を 1 回呼ぶ代わりに ai.runMany(...) を使ってください — 並行数の上限と 項目ごとの進捗をあなたの代わりに処理してくれます。SDK のより詳しい Integration Guide(公開されて いないため、必要な場合はプラットフォームの運営者にコピーを依頼してください)に、オプションの 完全な形(concurrencyerrorStrategyonProgress)とパイプラインごとの結果形式のテーブルが 記載されています。

エラー処理

ai.run()FAILED/CANCELED チケットと HTTP エラーの両方で reject します — 上記のフックは すでにその両方を state.error にルーティングしています。認証済みの経路が返しうるステータス コードの完全な一覧については、ツールリファレンス の Errors セクションを参照してください。