Bluesky app fork with some witchin' additions 💫

fix: add missing files for alt text gen & toggling handle in URLs

xan.lol f5c7d5af b7b1ab8a

verified
+214
+61
src/lib/ai/generateAltText.ts
··· 1 + import {DEFAULT_ALT_TEXT_AI_MODEL, MAX_ALT_TEXT} from '#/lib/constants' 2 + import {logger} from '#/logger' 3 + 4 + export async function generateAltText( 5 + apiKey: string, 6 + model: string, 7 + imageBase64: string, 8 + imageMimeType: string, 9 + ): Promise<string> { 10 + const response = await fetch( 11 + 'https://openrouter.ai/api/v1/chat/completions', 12 + { 13 + method: 'POST', 14 + headers: { 15 + Authorization: `Bearer ${apiKey}`, 16 + 'Content-Type': 'application/json', 17 + 'HTTP-Referer': 'https://witchsky.app', 18 + 'X-Title': 'Witchsky', 19 + }, 20 + body: JSON.stringify({ 21 + model: model || DEFAULT_ALT_TEXT_AI_MODEL, 22 + messages: [ 23 + { 24 + role: 'user', 25 + content: [ 26 + { 27 + type: 'text', 28 + 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.`, 29 + }, 30 + { 31 + type: 'image_url', 32 + image_url: { 33 + url: `data:${imageMimeType};base64,${imageBase64}`, 34 + }, 35 + }, 36 + ], 37 + }, 38 + ], 39 + max_tokens: MAX_ALT_TEXT, 40 + }), 41 + }, 42 + ) 43 + 44 + if (!response.ok) { 45 + const errorText = await response.text() 46 + logger.error('OpenRouter API error', { 47 + status: response.status, 48 + error: errorText, 49 + }) 50 + throw new Error(`OpenRouter API error: ${response.status}`) 51 + } 52 + 53 + const data = await response.json() 54 + const altText = data.choices?.[0]?.message?.content?.trim() 55 + 56 + if (!altText) { 57 + throw new Error('No alt text generated') 58 + } 59 + 60 + return altText 61 + }
+91
src/state/preferences/openrouter.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type ApiKeyStateContext = persisted.Schema['openRouterApiKey'] 6 + type SetApiKeyContext = (v: persisted.Schema['openRouterApiKey']) => void 7 + type ModelStateContext = persisted.Schema['openRouterModel'] 8 + type SetModelContext = (v: persisted.Schema['openRouterModel']) => void 9 + 10 + const apiKeyStateContext = React.createContext<ApiKeyStateContext>( 11 + persisted.defaults.openRouterApiKey, 12 + ) 13 + const setApiKeyContext = React.createContext<SetApiKeyContext>( 14 + (_: persisted.Schema['openRouterApiKey']) => {}, 15 + ) 16 + const modelStateContext = React.createContext<ModelStateContext>( 17 + persisted.defaults.openRouterModel, 18 + ) 19 + const setModelContext = React.createContext<SetModelContext>( 20 + (_: persisted.Schema['openRouterModel']) => {}, 21 + ) 22 + 23 + export function Provider({children}: React.PropsWithChildren<{}>) { 24 + const [apiKeyState, setApiKeyState] = React.useState( 25 + persisted.get('openRouterApiKey'), 26 + ) 27 + const [modelState, setModelState] = React.useState( 28 + persisted.get('openRouterModel'), 29 + ) 30 + 31 + const setApiKeyWrapped = React.useCallback( 32 + (openRouterApiKey: persisted.Schema['openRouterApiKey']) => { 33 + setApiKeyState(openRouterApiKey) 34 + persisted.write('openRouterApiKey', openRouterApiKey) 35 + }, 36 + [setApiKeyState], 37 + ) 38 + 39 + const setModelWrapped = React.useCallback( 40 + (openRouterModel: persisted.Schema['openRouterModel']) => { 41 + setModelState(openRouterModel) 42 + persisted.write('openRouterModel', openRouterModel) 43 + }, 44 + [setModelState], 45 + ) 46 + 47 + React.useEffect(() => { 48 + return persisted.onUpdate('openRouterApiKey', nextApiKey => { 49 + setApiKeyState(nextApiKey) 50 + }) 51 + }, [setApiKeyWrapped]) 52 + 53 + React.useEffect(() => { 54 + return persisted.onUpdate('openRouterModel', nextModel => { 55 + setModelState(nextModel) 56 + }) 57 + }, [setModelWrapped]) 58 + 59 + return ( 60 + <apiKeyStateContext.Provider value={apiKeyState}> 61 + <setApiKeyContext.Provider value={setApiKeyWrapped}> 62 + <modelStateContext.Provider value={modelState}> 63 + <setModelContext.Provider value={setModelWrapped}> 64 + {children} 65 + </setModelContext.Provider> 66 + </modelStateContext.Provider> 67 + </setApiKeyContext.Provider> 68 + </apiKeyStateContext.Provider> 69 + ) 70 + } 71 + 72 + export function useOpenRouterApiKey() { 73 + return React.useContext(apiKeyStateContext) 74 + } 75 + 76 + export function useSetOpenRouterApiKey() { 77 + return React.useContext(setApiKeyContext) 78 + } 79 + 80 + export function useOpenRouterModel() { 81 + return React.useContext(modelStateContext) 82 + } 83 + 84 + export function useSetOpenRouterModel() { 85 + return React.useContext(setModelContext) 86 + } 87 + 88 + export function useOpenRouterConfigured() { 89 + const apiKey = useOpenRouterApiKey() 90 + return !!apiKey && apiKey.length > 0 91 + }
+62
src/state/preferences/use-handle-in-links.tsx
··· 1 + import React from 'react' 2 + import {reloadAppAsync} from 'expo' 3 + 4 + import * as persisted from '#/state/persisted' 5 + import {IS_WEB} from '#/env' 6 + 7 + type StateContext = persisted.Schema['useHandleInLinks'] 8 + type SetContext = (v: persisted.Schema['useHandleInLinks']) => void 9 + 10 + const stateContext = React.createContext<StateContext>( 11 + persisted.defaults.useHandleInLinks, 12 + ) 13 + const setContext = React.createContext<SetContext>( 14 + (_: persisted.Schema['useHandleInLinks']) => {}, 15 + ) 16 + 17 + export function Provider({children}: React.PropsWithChildren<{}>) { 18 + const [state, setState] = React.useState(persisted.get('useHandleInLinks')) 19 + 20 + const setStateWrapped = React.useCallback( 21 + (useHandleInLinks: persisted.Schema['useHandleInLinks']) => { 22 + setState(useHandleInLinks) 23 + persisted.write('useHandleInLinks', useHandleInLinks) 24 + }, 25 + [setState], 26 + ) 27 + 28 + React.useEffect(() => { 29 + return persisted.onUpdate('useHandleInLinks', nextUseHandleInLinks => { 30 + setState(nextUseHandleInLinks) 31 + }) 32 + }, [setStateWrapped]) 33 + 34 + return ( 35 + <stateContext.Provider value={state}> 36 + <setContext.Provider value={setStateWrapped}> 37 + {children} 38 + </setContext.Provider> 39 + </stateContext.Provider> 40 + ) 41 + } 42 + 43 + export function useHandleInLinks() { 44 + return React.useContext(stateContext) 45 + } 46 + 47 + export function useSetHandleInLinks() { 48 + const set = React.useContext(setContext) 49 + 50 + return React.useCallback( 51 + (useHandleInLinks: persisted.Schema['useHandleInLinks']) => { 52 + set(useHandleInLinks) 53 + 54 + if (IS_WEB) { 55 + window.location.reload() 56 + } else { 57 + void reloadAppAsync() 58 + } 59 + }, 60 + [set], 61 + ) 62 + }