Bluesky app fork with some witchin' additions 💫
0
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`)}>