Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow customizing the alt text prompt

authored by

ave and committed by tangled.org 2f59a35f 873d4cd6

+153 -5
+7 -2
src/lib/ai/generateAltText.ts
··· 1 - import {DEFAULT_ALT_TEXT_AI_MODEL, MAX_ALT_TEXT} from '#/lib/constants' 1 + import { 2 + DEFAULT_ALT_TEXT_AI_MODEL, 3 + DEFAULT_ALT_TEXT_AI_PROMPT, 4 + MAX_ALT_TEXT, 5 + } from '#/lib/constants' 2 6 import {logger} from '#/logger' 3 7 4 8 export async function generateAltText( ··· 6 10 model: string, 7 11 imageBase64: string, 8 12 imageMimeType: string, 13 + customPrompt?: string, 9 14 ): Promise<string> { 10 15 const response = await fetch( 11 16 'https://openrouter.ai/api/v1/chat/completions', ··· 25 30 content: [ 26 31 { 27 32 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.`, 33 + text: customPrompt || DEFAULT_ALT_TEXT_AI_PROMPT, 29 34 }, 30 35 { 31 36 type: 'image_url',
+102 -1
src/screens/Settings/RunesSettings.tsx
··· 7 7 import {Trans} from '@lingui/react/macro' 8 8 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 9 9 10 - import {DEFAULT_ALT_TEXT_AI_MODEL} from '#/lib/constants' 10 + import { 11 + DEFAULT_ALT_TEXT_AI_MODEL, 12 + DEFAULT_ALT_TEXT_AI_PROMPT, 13 + } from '#/lib/constants' 11 14 import {usePalette} from '#/lib/hooks/usePalette' 12 15 import {type CommonNavigatorParams} from '#/lib/routes/types' 13 16 import {dynamicActivate} from '#/locale/i18n' ··· 121 124 useOpenRouterApiKey, 122 125 useOpenRouterConfigured, 123 126 useOpenRouterModel, 127 + useOpenRouterPrompt, 124 128 useSetOpenRouterApiKey, 125 129 useSetOpenRouterModel, 130 + useSetOpenRouterPrompt, 126 131 } from '#/state/preferences/openrouter' 127 132 import { 128 133 usePdsLabelEnabled, ··· 865 870 ) 866 871 } 867 872 873 + function OpenRouterPromptDialog({ 874 + control, 875 + }: { 876 + control: Dialog.DialogControlProps 877 + }) { 878 + const pal = usePalette('default') 879 + const {_} = useLingui() 880 + 881 + const prompt = useOpenRouterPrompt() 882 + const [value, setValue] = useState(prompt ?? '') 883 + const setPrompt = useSetOpenRouterPrompt() 884 + 885 + const submit = () => { 886 + setPrompt(value.trim() || undefined) 887 + control.close() 888 + } 889 + 890 + return ( 891 + <Dialog.Outer 892 + control={control} 893 + nativeOptions={{preventExpansion: true}} 894 + onClose={() => setValue(prompt ?? '')}> 895 + <Dialog.Handle /> 896 + <Dialog.ScrollableInner label={_(msg`Alt Text Prompt`)}> 897 + <View style={[a.gap_sm, a.pb_lg]}> 898 + <Text style={[a.text_2xl, a.font_bold]}> 899 + <Trans>Alt Text Prompt</Trans> 900 + </Text> 901 + </View> 902 + 903 + <View style={a.gap_lg}> 904 + <Dialog.Input 905 + label="Prompt" 906 + multiline 907 + numberOfLines={6} 908 + style={[ 909 + styles.textInput, 910 + pal.border, 911 + pal.text, 912 + {minHeight: 120, textAlignVertical: 'top'}, 913 + ]} 914 + onChangeText={setValue} 915 + placeholder={DEFAULT_ALT_TEXT_AI_PROMPT} 916 + placeholderTextColor={pal.colors.textLight} 917 + accessibilityHint={_( 918 + msg`Enter a custom prompt for AI alt text generation`, 919 + )} 920 + defaultValue={prompt ?? ''} 921 + /> 922 + 923 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 924 + <Button 925 + label={_(msg`Save`)} 926 + size="large" 927 + onPress={submit} 928 + variant="solid" 929 + color="primary"> 930 + <ButtonText> 931 + <Trans>Save</Trans> 932 + </ButtonText> 933 + </Button> 934 + </View> 935 + </View> 936 + 937 + <Dialog.Close /> 938 + </Dialog.ScrollableInner> 939 + </Dialog.Outer> 940 + ) 941 + } 942 + 868 943 export function RunesSettingsScreen({}: Props) { 869 944 const {_} = useLingui() 870 945 ··· 972 1047 const setOpenRouterApiKeyControl = Dialog.useDialogControl() 973 1048 const openRouterModel = useOpenRouterModel() 974 1049 const setOpenRouterModelControl = Dialog.useDialogControl() 1050 + const setOpenRouterPromptControl = Dialog.useDialogControl() 975 1051 const openRouterConfigured = useOpenRouterConfigured() 976 1052 977 1053 const autoLikeOnRepost = useAutoLikeOnRepost() ··· 1487 1563 </SettingsList.Item> 1488 1564 )} 1489 1565 1566 + {openRouterConfigured && ( 1567 + <SettingsList.Item> 1568 + <SettingsList.ItemIcon icon={_BeakerIcon} /> 1569 + <SettingsList.ItemText> 1570 + <Trans>Alt Text Prompt</Trans> 1571 + </SettingsList.ItemText> 1572 + <SettingsList.BadgeButton 1573 + label={_(msg`Change`)} 1574 + onPress={() => setOpenRouterPromptControl.open()} 1575 + /> 1576 + </SettingsList.Item> 1577 + )} 1578 + 1579 + {openRouterConfigured && ( 1580 + <SettingsList.Item> 1581 + <Admonition type="info" style={[a.flex_1]}> 1582 + <Trans> 1583 + Customize the prompt sent to the AI model when generating alt 1584 + text. Leave empty to use the default prompt. 1585 + </Trans> 1586 + </Admonition> 1587 + </SettingsList.Item> 1588 + )} 1589 + 1490 1590 <SettingsList.Divider /> 1491 1591 1492 1592 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> ··· 1707 1807 <PostReplacementDialog control={setPostReplacementDialogControl} /> 1708 1808 <OpenRouterApiKeyDialog control={setOpenRouterApiKeyControl} /> 1709 1809 <OpenRouterModelDialog control={setOpenRouterModelControl} /> 1810 + <OpenRouterPromptDialog control={setOpenRouterPromptControl} /> 1710 1811 </Layout.Screen> 1711 1812 ) 1712 1813 }
+2
src/state/persisted/schema.ts
··· 201 201 202 202 openRouterApiKey: z.string().optional(), 203 203 openRouterModel: z.string().optional(), 204 + openRouterPrompt: z.string().optional(), 204 205 205 206 useHandleInLinks: z.boolean().optional(), 206 207 ··· 327 328 328 329 openRouterApiKey: undefined, 329 330 openRouterModel: DEFAULT_ALT_TEXT_AI_MODEL, 331 + openRouterPrompt: undefined, 330 332 331 333 useHandleInLinks: false, 332 334
+38 -1
src/state/preferences/openrouter.tsx
··· 6 6 type SetApiKeyContext = (v: persisted.Schema['openRouterApiKey']) => void 7 7 type ModelStateContext = persisted.Schema['openRouterModel'] 8 8 type SetModelContext = (v: persisted.Schema['openRouterModel']) => void 9 + type PromptStateContext = persisted.Schema['openRouterPrompt'] 10 + type SetPromptContext = (v: persisted.Schema['openRouterPrompt']) => void 9 11 10 12 const apiKeyStateContext = React.createContext<ApiKeyStateContext>( 11 13 persisted.defaults.openRouterApiKey, ··· 19 21 const setModelContext = React.createContext<SetModelContext>( 20 22 (_: persisted.Schema['openRouterModel']) => {}, 21 23 ) 24 + const promptStateContext = React.createContext<PromptStateContext>( 25 + persisted.defaults.openRouterPrompt, 26 + ) 27 + const setPromptContext = React.createContext<SetPromptContext>( 28 + (_: persisted.Schema['openRouterPrompt']) => {}, 29 + ) 22 30 23 31 export function Provider({children}: React.PropsWithChildren<{}>) { 24 32 const [apiKeyState, setApiKeyState] = React.useState( ··· 27 35 const [modelState, setModelState] = React.useState( 28 36 persisted.get('openRouterModel'), 29 37 ) 38 + const [promptState, setPromptState] = React.useState( 39 + persisted.get('openRouterPrompt'), 40 + ) 30 41 31 42 const setApiKeyWrapped = React.useCallback( 32 43 (openRouterApiKey: persisted.Schema['openRouterApiKey']) => { ··· 44 55 [setModelState], 45 56 ) 46 57 58 + const setPromptWrapped = React.useCallback( 59 + (openRouterPrompt: persisted.Schema['openRouterPrompt']) => { 60 + setPromptState(openRouterPrompt) 61 + persisted.write('openRouterPrompt', openRouterPrompt) 62 + }, 63 + [setPromptState], 64 + ) 65 + 47 66 React.useEffect(() => { 48 67 return persisted.onUpdate('openRouterApiKey', nextApiKey => { 49 68 setApiKeyState(nextApiKey) ··· 56 75 }) 57 76 }, [setModelWrapped]) 58 77 78 + React.useEffect(() => { 79 + return persisted.onUpdate('openRouterPrompt', nextPrompt => { 80 + setPromptState(nextPrompt) 81 + }) 82 + }, [setPromptWrapped]) 83 + 59 84 return ( 60 85 <apiKeyStateContext.Provider value={apiKeyState}> 61 86 <setApiKeyContext.Provider value={setApiKeyWrapped}> 62 87 <modelStateContext.Provider value={modelState}> 63 88 <setModelContext.Provider value={setModelWrapped}> 64 - {children} 89 + <promptStateContext.Provider value={promptState}> 90 + <setPromptContext.Provider value={setPromptWrapped}> 91 + {children} 92 + </setPromptContext.Provider> 93 + </promptStateContext.Provider> 65 94 </setModelContext.Provider> 66 95 </modelStateContext.Provider> 67 96 </setApiKeyContext.Provider> ··· 83 112 84 113 export function useSetOpenRouterModel() { 85 114 return React.useContext(setModelContext) 115 + } 116 + 117 + export function useOpenRouterPrompt() { 118 + return React.useContext(promptStateContext) 119 + } 120 + 121 + export function useSetOpenRouterPrompt() { 122 + return React.useContext(setPromptContext) 86 123 } 87 124 88 125 export function useOpenRouterConfigured() {
+4 -1
src/view/com/composer/photos/ImageAltTextDialog.tsx
··· 19 19 useOpenRouterApiKey, 20 20 useOpenRouterConfigured, 21 21 useOpenRouterModel, 22 + useOpenRouterPrompt, 22 23 } from '#/state/preferences/openrouter' 23 24 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' 24 25 import {atoms as a, tokens, useTheme} from '#/alf' ··· 92 93 const openRouterConfigured = useOpenRouterConfigured() 93 94 const openRouterApiKey = useOpenRouterApiKey() 94 95 const openRouterModel = useOpenRouterModel() 96 + const openRouterPrompt = useOpenRouterPrompt() 95 97 96 98 const imageStyle = useMemo<ImageStyle>(() => { 97 99 const maxWidth = IS_WEB ··· 153 155 openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL, 154 156 base64, 155 157 mimeType, 158 + openRouterPrompt ?? undefined, 156 159 ) 157 160 158 161 setAltText(enforceLen(generated, MAX_ALT_TEXT, true)) ··· 163 166 } finally { 164 167 setIsGenerating(false) 165 168 } 166 - }, [openRouterApiKey, openRouterModel, image, setAltText]) 169 + }, [openRouterApiKey, openRouterModel, openRouterPrompt, image, setAltText]) 167 170 168 171 return ( 169 172 <Dialog.ScrollableInner label={_(msg`Add alt text`)}>