From f5c7d5af68d4808dc6a7b2f6bc201e275ad38c35 Mon Sep 17 00:00:00 2001 From: "xan.lol" Date: Tue, 17 Feb 2026 00:40:57 -0800 Subject: [PATCH] fix: add missing files for alt text gen & toggling handle in URLs --- src/lib/ai/generateAltText.ts | 61 +++++++++++++ src/state/preferences/openrouter.tsx | 91 +++++++++++++++++++ src/state/preferences/use-handle-in-links.tsx | 62 +++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 src/lib/ai/generateAltText.ts create mode 100644 src/state/preferences/openrouter.tsx create mode 100644 src/state/preferences/use-handle-in-links.tsx diff --git a/src/lib/ai/generateAltText.ts b/src/lib/ai/generateAltText.ts new file mode 100644 index 000000000..232854138 --- /dev/null +++ b/src/lib/ai/generateAltText.ts @@ -0,0 +1,61 @@ +import {DEFAULT_ALT_TEXT_AI_MODEL, MAX_ALT_TEXT} from '#/lib/constants' +import {logger} from '#/logger' + +export async function generateAltText( + apiKey: string, + model: string, + imageBase64: string, + imageMimeType: string, +): Promise { + const response = await fetch( + 'https://openrouter.ai/api/v1/chat/completions', + { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'HTTP-Referer': 'https://witchsky.app', + 'X-Title': 'Witchsky', + }, + body: JSON.stringify({ + model: model || DEFAULT_ALT_TEXT_AI_MODEL, + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: `Generate a concise, descriptive alt text for this image, also extract text if needed. The alt text should be clear and helpful for screen readers. Keep it under ${MAX_ALT_TEXT} characters. Only respond with the alt text itself, no explanations or quotes.`, + }, + { + type: 'image_url', + image_url: { + url: `data:${imageMimeType};base64,${imageBase64}`, + }, + }, + ], + }, + ], + max_tokens: MAX_ALT_TEXT, + }), + }, + ) + + if (!response.ok) { + const errorText = await response.text() + logger.error('OpenRouter API error', { + status: response.status, + error: errorText, + }) + throw new Error(`OpenRouter API error: ${response.status}`) + } + + const data = await response.json() + const altText = data.choices?.[0]?.message?.content?.trim() + + if (!altText) { + throw new Error('No alt text generated') + } + + return altText +} diff --git a/src/state/preferences/openrouter.tsx b/src/state/preferences/openrouter.tsx new file mode 100644 index 000000000..a1eea4feb --- /dev/null +++ b/src/state/preferences/openrouter.tsx @@ -0,0 +1,91 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type ApiKeyStateContext = persisted.Schema['openRouterApiKey'] +type SetApiKeyContext = (v: persisted.Schema['openRouterApiKey']) => void +type ModelStateContext = persisted.Schema['openRouterModel'] +type SetModelContext = (v: persisted.Schema['openRouterModel']) => void + +const apiKeyStateContext = React.createContext( + persisted.defaults.openRouterApiKey, +) +const setApiKeyContext = React.createContext( + (_: persisted.Schema['openRouterApiKey']) => {}, +) +const modelStateContext = React.createContext( + persisted.defaults.openRouterModel, +) +const setModelContext = React.createContext( + (_: persisted.Schema['openRouterModel']) => {}, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [apiKeyState, setApiKeyState] = React.useState( + persisted.get('openRouterApiKey'), + ) + const [modelState, setModelState] = React.useState( + persisted.get('openRouterModel'), + ) + + const setApiKeyWrapped = React.useCallback( + (openRouterApiKey: persisted.Schema['openRouterApiKey']) => { + setApiKeyState(openRouterApiKey) + persisted.write('openRouterApiKey', openRouterApiKey) + }, + [setApiKeyState], + ) + + const setModelWrapped = React.useCallback( + (openRouterModel: persisted.Schema['openRouterModel']) => { + setModelState(openRouterModel) + persisted.write('openRouterModel', openRouterModel) + }, + [setModelState], + ) + + React.useEffect(() => { + return persisted.onUpdate('openRouterApiKey', nextApiKey => { + setApiKeyState(nextApiKey) + }) + }, [setApiKeyWrapped]) + + React.useEffect(() => { + return persisted.onUpdate('openRouterModel', nextModel => { + setModelState(nextModel) + }) + }, [setModelWrapped]) + + return ( + + + + + {children} + + + + + ) +} + +export function useOpenRouterApiKey() { + return React.useContext(apiKeyStateContext) +} + +export function useSetOpenRouterApiKey() { + return React.useContext(setApiKeyContext) +} + +export function useOpenRouterModel() { + return React.useContext(modelStateContext) +} + +export function useSetOpenRouterModel() { + return React.useContext(setModelContext) +} + +export function useOpenRouterConfigured() { + const apiKey = useOpenRouterApiKey() + return !!apiKey && apiKey.length > 0 +} diff --git a/src/state/preferences/use-handle-in-links.tsx b/src/state/preferences/use-handle-in-links.tsx new file mode 100644 index 000000000..9116b1edc --- /dev/null +++ b/src/state/preferences/use-handle-in-links.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import {reloadAppAsync} from 'expo' + +import * as persisted from '#/state/persisted' +import {IS_WEB} from '#/env' + +type StateContext = persisted.Schema['useHandleInLinks'] +type SetContext = (v: persisted.Schema['useHandleInLinks']) => void + +const stateContext = React.createContext( + persisted.defaults.useHandleInLinks, +) +const setContext = React.createContext( + (_: persisted.Schema['useHandleInLinks']) => {}, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState(persisted.get('useHandleInLinks')) + + const setStateWrapped = React.useCallback( + (useHandleInLinks: persisted.Schema['useHandleInLinks']) => { + setState(useHandleInLinks) + persisted.write('useHandleInLinks', useHandleInLinks) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate('useHandleInLinks', nextUseHandleInLinks => { + setState(nextUseHandleInLinks) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useHandleInLinks() { + return React.useContext(stateContext) +} + +export function useSetHandleInLinks() { + const set = React.useContext(setContext) + + return React.useCallback( + (useHandleInLinks: persisted.Schema['useHandleInLinks']) => { + set(useHandleInLinks) + + if (IS_WEB) { + window.location.reload() + } else { + void reloadAppAsync() + } + }, + [set], + ) +} -- 2.51.2