"content": "import { useChat } from \"@ai-sdk/react\";\nimport {\n DefaultChatTransport,\n lastAssistantMessageIsCompleteWithToolCalls,\n} from \"ai\";\nimport { useMemo } from \"react\";\n\nexport interface AskAIConfig {\n applicationId: string;\n apiKey: string;\n indexName: string;\n assistantId: string;\n}\n\nexport function useAskai(config: AskAIConfig) {\n if (!config) {\n throw new Error(\"config is required for useAskai\");\n }\n\n const baseUrl = \"https://askai.algolia.com\";\n\n const transport = useMemo(() => {\n return new DefaultChatTransport({\n api: `${baseUrl}/chat`,\n headers: async () => {\n const token = await getValidToken({ assistantId: config.assistantId });\n return {\n \"x-algolia-api-key\": config.apiKey,\n \"x-algolia-application-id\": config.applicationId,\n \"x-algolia-index-name\": config.indexName,\n \"x-algolia-assistant-id\": config.assistantId,\n \"x-ai-sdk-version\": \"v5\",\n authorization: `TOKEN ${token}`,\n } as Record<string, string>;\n },\n });\n }, [\n config.apiKey,\n config.applicationId,\n config.indexName,\n config.assistantId,\n ]);\n\n const chat = useChat({\n transport,\n sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,\n async onToolCall({ toolCall }) {\n if (toolCall.dynamic) return;\n },\n });\n\n const isGenerating =\n chat.status === \"submitted\" || chat.status === \"streaming\";\n\n return {\n ...chat,\n isGenerating,\n };\n}\n\nconst BASE_ASKAI_URL = \"https://askai.algolia.com\";\nconst TOKEN_KEY = \"askai_token\";\n\ntype TokenPayload = { exp: number };\n\nconst decode = (token: string): TokenPayload => {\n const [b64] = token.split(\".\");\n return JSON.parse(atob(b64));\n};\n\nconst isExpired = (token?: string | null): boolean => {\n if (!token) return true;\n try {\n const { exp } = decode(token);\n // refresh 30 s before the backend rejects it\n return Date.now() / 1000 > exp - 30;\n } catch {\n return true;\n }\n};\n\nlet inflight: Promise<string> | null = null;\n\n// call /token once, cache the promise while it’s running\n// eslint-disable-next-line require-await\nexport const getValidToken = async ({\n assistantId,\n}: {\n assistantId: string;\n}): Promise<string> => {\n const cached = sessionStorage.getItem(TOKEN_KEY);\n if (!isExpired(cached)) return cached as string;\n\n if (!inflight) {\n inflight = fetch(`${BASE_ASKAI_URL}/chat/token`, {\n method: \"POST\",\n headers: {\n \"x-algolia-assistant-id\": assistantId,\n \"content-type\": \"application/json\",\n },\n })\n .then((r) => r.json())\n .then(({ token }) => {\n sessionStorage.setItem(TOKEN_KEY, token);\n return token;\n })\n .finally(() => {\n inflight = null;\n });\n }\n\n return inflight;\n};\n\nexport const postFeedback = async ({\n assistantId,\n thumbs,\n messageId,\n appId,\n}: {\n assistantId: string;\n thumbs: 0 | 1;\n messageId: string;\n appId: string;\n}): Promise<Response> => {\n const headers = new Headers();\n headers.set(\"x-algolia-assistant-id\", assistantId);\n headers.set(\"content-type\", \"application/json\");\n\n const token = await getValidToken({ assistantId });\n headers.set(\"authorization\", `TOKEN ${token}`);\n\n return fetch(`${BASE_ASKAI_URL}/chat/feedback`, {\n method: \"POST\",\n body: JSON.stringify({\n appId,\n messageId,\n thumbs,\n }),\n headers,\n });\n};\n",
0 commit comments