From 2f59a35fe8c6c4e7a12780773f2e62dd706f6ebf Mon Sep 17 00:00:00 2001 From: ave Date: Wed, 8 Apr 2026 18:21:09 +0200 Subject: [PATCH] Allow customizing the alt text prompt --- src/lib/ai/generateAltText.ts | 9 +- src/screens/Settings/RunesSettings.tsx | 103 +++++++++++++++++- src/state/persisted/schema.ts | 2 + src/state/preferences/openrouter.tsx | 39 ++++++- .../composer/photos/ImageAltTextDialog.tsx | 5 +- 5 files changed, 153 insertions(+), 5 deletions(-) diff --git a/src/lib/ai/generateAltText.ts b/src/lib/ai/generateAltText.ts index 232854138..dc449bd7e 100644 --- a/src/lib/ai/generateAltText.ts +++ b/src/lib/ai/generateAltText.ts @@ -1,4 +1,8 @@ -import {DEFAULT_ALT_TEXT_AI_MODEL, MAX_ALT_TEXT} from '#/lib/constants' +import { + DEFAULT_ALT_TEXT_AI_MODEL, + DEFAULT_ALT_TEXT_AI_PROMPT, + MAX_ALT_TEXT, +} from '#/lib/constants' import {logger} from '#/logger' export async function generateAltText( @@ -6,6 +10,7 @@ export async function generateAltText( model: string, imageBase64: string, imageMimeType: string, + customPrompt?: string, ): Promise { const response = await fetch( 'https://openrouter.ai/api/v1/chat/completions', @@ -25,7 +30,7 @@ export async function generateAltText( 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.`, + text: customPrompt || DEFAULT_ALT_TEXT_AI_PROMPT, }, { type: 'image_url', diff --git a/src/screens/Settings/RunesSettings.tsx b/src/screens/Settings/RunesSettings.tsx index 2e1eb0364..d4e2e2959 100644 --- a/src/screens/Settings/RunesSettings.tsx +++ b/src/screens/Settings/RunesSettings.tsx @@ -7,7 +7,10 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {type NativeStackScreenProps} from '@react-navigation/native-stack' -import {DEFAULT_ALT_TEXT_AI_MODEL} from '#/lib/constants' +import { + DEFAULT_ALT_TEXT_AI_MODEL, + DEFAULT_ALT_TEXT_AI_PROMPT, +} from '#/lib/constants' import {usePalette} from '#/lib/hooks/usePalette' import {type CommonNavigatorParams} from '#/lib/routes/types' import {dynamicActivate} from '#/locale/i18n' @@ -121,8 +124,10 @@ import { useOpenRouterApiKey, useOpenRouterConfigured, useOpenRouterModel, + useOpenRouterPrompt, useSetOpenRouterApiKey, useSetOpenRouterModel, + useSetOpenRouterPrompt, } from '#/state/preferences/openrouter' import { usePdsLabelEnabled, @@ -865,6 +870,76 @@ function OpenRouterModelDialog({ ) } +function OpenRouterPromptDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {_} = useLingui() + + const prompt = useOpenRouterPrompt() + const [value, setValue] = useState(prompt ?? '') + const setPrompt = useSetOpenRouterPrompt() + + const submit = () => { + setPrompt(value.trim() || undefined) + control.close() + } + + return ( + setValue(prompt ?? '')}> + + + + + Alt Text Prompt + + + + + + + + + + + + + + + ) +} + export function RunesSettingsScreen({}: Props) { const {_} = useLingui() @@ -972,6 +1047,7 @@ export function RunesSettingsScreen({}: Props) { const setOpenRouterApiKeyControl = Dialog.useDialogControl() const openRouterModel = useOpenRouterModel() const setOpenRouterModelControl = Dialog.useDialogControl() + const setOpenRouterPromptControl = Dialog.useDialogControl() const openRouterConfigured = useOpenRouterConfigured() const autoLikeOnRepost = useAutoLikeOnRepost() @@ -1487,6 +1563,30 @@ export function RunesSettingsScreen({}: Props) { )} + {openRouterConfigured && ( + + + + Alt Text Prompt + + setOpenRouterPromptControl.open()} + /> + + )} + + {openRouterConfigured && ( + + + + Customize the prompt sent to the AI model when generating alt + text. Leave empty to use the default prompt. + + + + )} + @@ -1707,6 +1807,7 @@ export function RunesSettingsScreen({}: Props) { + ) } diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 4ee801065..16b44ef0c 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -201,6 +201,7 @@ const schema = z.object({ openRouterApiKey: z.string().optional(), openRouterModel: z.string().optional(), + openRouterPrompt: z.string().optional(), useHandleInLinks: z.boolean().optional(), @@ -327,6 +328,7 @@ export const defaults: Schema = { openRouterApiKey: undefined, openRouterModel: DEFAULT_ALT_TEXT_AI_MODEL, + openRouterPrompt: undefined, useHandleInLinks: false, diff --git a/src/state/preferences/openrouter.tsx b/src/state/preferences/openrouter.tsx index a1eea4feb..fdd81cb08 100644 --- a/src/state/preferences/openrouter.tsx +++ b/src/state/preferences/openrouter.tsx @@ -6,6 +6,8 @@ type ApiKeyStateContext = persisted.Schema['openRouterApiKey'] type SetApiKeyContext = (v: persisted.Schema['openRouterApiKey']) => void type ModelStateContext = persisted.Schema['openRouterModel'] type SetModelContext = (v: persisted.Schema['openRouterModel']) => void +type PromptStateContext = persisted.Schema['openRouterPrompt'] +type SetPromptContext = (v: persisted.Schema['openRouterPrompt']) => void const apiKeyStateContext = React.createContext( persisted.defaults.openRouterApiKey, @@ -19,6 +21,12 @@ const modelStateContext = React.createContext( const setModelContext = React.createContext( (_: persisted.Schema['openRouterModel']) => {}, ) +const promptStateContext = React.createContext( + persisted.defaults.openRouterPrompt, +) +const setPromptContext = React.createContext( + (_: persisted.Schema['openRouterPrompt']) => {}, +) export function Provider({children}: React.PropsWithChildren<{}>) { const [apiKeyState, setApiKeyState] = React.useState( @@ -27,6 +35,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const [modelState, setModelState] = React.useState( persisted.get('openRouterModel'), ) + const [promptState, setPromptState] = React.useState( + persisted.get('openRouterPrompt'), + ) const setApiKeyWrapped = React.useCallback( (openRouterApiKey: persisted.Schema['openRouterApiKey']) => { @@ -44,6 +55,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [setModelState], ) + const setPromptWrapped = React.useCallback( + (openRouterPrompt: persisted.Schema['openRouterPrompt']) => { + setPromptState(openRouterPrompt) + persisted.write('openRouterPrompt', openRouterPrompt) + }, + [setPromptState], + ) + React.useEffect(() => { return persisted.onUpdate('openRouterApiKey', nextApiKey => { setApiKeyState(nextApiKey) @@ -56,12 +75,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }) }, [setModelWrapped]) + React.useEffect(() => { + return persisted.onUpdate('openRouterPrompt', nextPrompt => { + setPromptState(nextPrompt) + }) + }, [setPromptWrapped]) + return ( - {children} + + + {children} + + @@ -85,6 +114,14 @@ export function useSetOpenRouterModel() { return React.useContext(setModelContext) } +export function useOpenRouterPrompt() { + return React.useContext(promptStateContext) +} + +export function useSetOpenRouterPrompt() { + return React.useContext(setPromptContext) +} + export function useOpenRouterConfigured() { const apiKey = useOpenRouterApiKey() return !!apiKey && apiKey.length > 0 diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx index 180d5bd1a..7b0ec00d5 100644 --- a/src/view/com/composer/photos/ImageAltTextDialog.tsx +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -19,6 +19,7 @@ import { useOpenRouterApiKey, useOpenRouterConfigured, useOpenRouterModel, + useOpenRouterPrompt, } from '#/state/preferences/openrouter' import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' import {atoms as a, tokens, useTheme} from '#/alf' @@ -92,6 +93,7 @@ const ImageAltTextInner = ({ const openRouterConfigured = useOpenRouterConfigured() const openRouterApiKey = useOpenRouterApiKey() const openRouterModel = useOpenRouterModel() + const openRouterPrompt = useOpenRouterPrompt() const imageStyle = useMemo(() => { const maxWidth = IS_WEB @@ -153,6 +155,7 @@ const ImageAltTextInner = ({ openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL, base64, mimeType, + openRouterPrompt ?? undefined, ) setAltText(enforceLen(generated, MAX_ALT_TEXT, true)) @@ -163,7 +166,7 @@ const ImageAltTextInner = ({ } finally { setIsGenerating(false) } - }, [openRouterApiKey, openRouterModel, image, setAltText]) + }, [openRouterApiKey, openRouterModel, openRouterPrompt, image, setAltText]) return ( -- 2.51.2