+
{inputImages.map((img, idx) => renderImageThumb(img, idx))}
{renderClearAllButton()}
@@ -1463,35 +1429,12 @@ export default function InputBar() {
-
@@ -1525,12 +1468,12 @@ export default function InputBar() {
@@ -1607,11 +1550,95 @@ export default function InputBar() {
max={outputImageLimit}
className="px-3 py-1.5 rounded-xl border border-gray-200/60 dark:border-white/[0.08] bg-white/50 dark:bg-white/[0.03] focus:outline-none text-xs transition-all duration-200 shadow-sm"
/>
-
+
)
+ const mobileChipClass = 'inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-medium border border-gray-200/60 dark:border-white/[0.08] bg-white/80 dark:bg-white/[0.06] text-gray-600 dark:text-gray-300 shadow-sm active:scale-95 transition-all whitespace-nowrap'
+
+ const renderMobileParamChips = () => (
+
+
+
+
+
+
+ )
+
+ const renderMobileActionSheet = () => {
+ if (!mobileParamSheet) return null
+ const configs = {
+ quality: {
+ title: '质量',
+ options: qualityOptions,
+ value: settings.codexCli ? 'auto' : isFalProvider && params.quality === 'auto' ? 'high' : params.quality,
+ onChange: (val: string) => setParams({ quality: val as any }),
+ },
+ format: {
+ title: '格式',
+ options: [{ label: 'PNG', value: 'png' }, { label: 'JPEG', value: 'jpeg' }, { label: 'WebP', value: 'webp' }],
+ value: params.output_format,
+ onChange: (val: string) => setParams({ output_format: val as any }),
+ },
+ moderation: {
+ title: '审核',
+ options: [{ label: 'auto', value: 'auto' }, { label: 'low', value: 'low' }],
+ value: params.moderation,
+ onChange: (val: string) => setParams({ moderation: val as any }),
+ },
+ }
+ const cfg = configs[mobileParamSheet]
+ return (
+
setMobileParamSheet(null)} onTouchEnd={() => setMobileParamSheet(null)}>
+
+
e.stopPropagation()}
+ onTouchEnd={(e) => e.stopPropagation()}
+ >
+
{cfg.title}
+
+ {cfg.options.map((opt) => (
+
+ ))}
+
+
+
+ )
+ }
+
return (
<>
{/* 全屏拖拽遮罩 */}
@@ -1657,6 +1684,33 @@ export default function InputBar() {
/>
)}
+ {showParamsModal && (
+
setShowParamsModal(false)}>
+
+
e.stopPropagation()}
+ >
+
+
参数设置
+
+
+ {renderParams('grid-cols-2')}
+
+
+ )}
+
+ {renderMobileActionSheet()}
+
{selectedTaskIds.length > 0 && (
@@ -1728,13 +1782,15 @@ export default function InputBar() {
)}
{/* 移动端拖动条 */}
-
setMobileCollapsed((v) => !v)}
- >
-
-
+ {!keyboardVisible && (
+
setMobileCollapsed((v) => !v)}
+ >
+
+
+ )}
{/* 输入图片行(移动端可折叠) */}
{inputImages.length > 0 && (
@@ -1756,190 +1812,296 @@ export default function InputBar() {
)
)}
- {/* 输入框 */}
-
- {showAtImageMenu && (
-
-
选择当前参考图
-
- {atImageOptions.map(({ img, index }, optionIndex) => (
-
- ))}
-
-
- )}
-
{
- isUserInputRef.current = true
- const el = e.currentTarget
- const range = getContentEditableSelection(el)
- setCursorPos(range.start)
- syncMentionTagSelection(el)
- const text = getContentEditablePlainText(el)
- setPrompt(text)
- setAtImageMenuIndex(0)
- setAtImageMenuDismissed(false)
- }}
- onSelect={(e) => {
- const el = e.currentTarget
- const range = getContentEditableSelection(el)
- setCursorPos(range.start)
- syncMentionTagSelection(el)
- setAtImageMenuIndex(0)
- setAtImageMenuDismissed(false)
- }}
- onKeyDown={handleKeyDown}
- onPaste={handlePromptPaste}
- onCopy={handlePromptCopy}
- onClick={(e) => {
- const el = textareaRef.current
- if (!el) return
- const target = e.target as HTMLElement
- if (target.classList.contains('mention-tag')) {
- const sel = window.getSelection()
- if (sel) {
- const range = document.createRange()
- range.selectNode(target)
- sel.removeAllRanges()
- sel.addRange(range)
- syncMentionTagSelection(el)
- }
- return
- }
-
- syncMentionTagSelection(el)
- }}
- data-placeholder="描述你想生成的图片,可输入 @ 指定当前参考图..."
- className="min-h-[42px] w-full whitespace-pre-wrap break-words rounded-2xl border border-gray-200/60 bg-white/50 px-4 py-3 text-sm leading-relaxed shadow-sm outline-none transition-[border-color,box-shadow] duration-200 focus:ring-1 focus:ring-blue-300/40 empty:before:pointer-events-none empty:before:text-gray-400 empty:before:content-[attr(data-placeholder)] dark:border-white/[0.08] dark:bg-white/[0.03] dark:text-gray-100 dark:focus:ring-blue-500/30 dark:empty:before:text-gray-500"
- />
-
-
- {/* 参数 + 按钮 */}
-
- {/* 桌面端布局 */}
-
- {renderParams('grid-cols-6')}
-
-
-
setAttachHover(true)}
- onMouseLeave={() => setAttachHover(false)}
- >
-
+ {/* 输入框 + 按钮 */}
+ {isMobile ? (
+
+ {!keyboardVisible && {renderMobileParamChips()}
}
+
+ {showAtImageMenu && (
+
+
选择当前参考图
+
+ {atImageOptions.map(({ img, index }, optionIndex) => (
+
+ ))}
+
+
+ )}
+
+ {!keyboardVisible &&
+
-
-
setSubmitHover(true)}
- onMouseLeave={() => setSubmitHover(false)}
- >
-
+
}
+
{
+ isUserInputRef.current = true
+ const el = e.currentTarget
+ const range = getContentEditableSelection(el)
+ setCursorPos(range.start)
+ syncMentionTagSelection(el)
+ const text = getContentEditablePlainText(el)
+ setPrompt(text)
+ setAtImageMenuIndex(0)
+ setAtImageMenuDismissed(false)
+ }}
+ onSelect={(e) => {
+ const el = e.currentTarget
+ const range = getContentEditableSelection(el)
+ setCursorPos(range.start)
+ syncMentionTagSelection(el)
+ setAtImageMenuIndex(0)
+ setAtImageMenuDismissed(false)
+ }}
+ onKeyDown={handleKeyDown}
+ onPaste={handlePromptPaste}
+ onCopy={handlePromptCopy}
+ onClick={(e) => {
+ const el = textareaRef.current
+ if (!el) return
+ const target = e.target as HTMLElement
+ if (target.classList.contains('mention-tag')) {
+ const sel = window.getSelection()
+ if (sel) {
+ const range = document.createRange()
+ range.selectNode(target)
+ sel.removeAllRanges()
+ sel.addRange(range)
+ syncMentionTagSelection(el)
+ }
+ return
+ }
+ syncMentionTagSelection(el)
+ }}
+ data-placeholder="描述你想生成的图片……"
+ style={{ lineHeight: `${MOBILE_INPUT_ROW_H}px` }}
+ className="flex-1 min-w-0 max-h-[120px] overflow-y-auto whitespace-pre-wrap break-words bg-transparent px-1 text-sm outline-none empty:before:pointer-events-none empty:before:text-gray-400 empty:before:content-[attr(data-placeholder)] dark:text-gray-100 dark:empty:before:text-gray-500"
+ />
-
-
- {/* 移动端布局 */}
-
-
-
- {renderParams('grid-cols-2')}
-
-
+
+ ) : (
+
+ {/* 桌面端输入框 */}
+
+ {showAtImageMenu && (
+
+
选择当前参考图
+
+ {atImageOptions.map(({ img, index }, optionIndex) => (
+
+ ))}
+
+
+ )}
+
{
+ isUserInputRef.current = true
+ const el = e.currentTarget
+ const range = getContentEditableSelection(el)
+ setCursorPos(range.start)
+ syncMentionTagSelection(el)
+ const text = getContentEditablePlainText(el)
+ setPrompt(text)
+ setAtImageMenuIndex(0)
+ setAtImageMenuDismissed(false)
+ }}
+ onSelect={(e) => {
+ const el = e.currentTarget
+ const range = getContentEditableSelection(el)
+ setCursorPos(range.start)
+ syncMentionTagSelection(el)
+ setAtImageMenuIndex(0)
+ setAtImageMenuDismissed(false)
+ }}
+ onKeyDown={handleKeyDown}
+ onPaste={handlePromptPaste}
+ onCopy={handlePromptCopy}
+ onClick={(e) => {
+ const el = textareaRef.current
+ if (!el) return
+ const target = e.target as HTMLElement
+ if (target.classList.contains('mention-tag')) {
+ const sel = window.getSelection()
+ if (sel) {
+ const range = document.createRange()
+ range.selectNode(target)
+ sel.removeAllRanges()
+ sel.addRange(range)
+ syncMentionTagSelection(el)
+ }
+ return
+ }
+ syncMentionTagSelection(el)
+ }}
+ data-placeholder="描述你想生成的图片……"
+ className="min-h-[42px] w-full whitespace-pre-wrap break-words rounded-2xl border border-gray-200/60 bg-white/50 px-4 py-3 text-sm leading-relaxed shadow-sm outline-none transition-[border-color,box-shadow] duration-200 focus:ring-1 focus:ring-blue-300/40 empty:before:pointer-events-none empty:before:text-gray-400 empty:before:content-[attr(data-placeholder)] dark:border-white/[0.08] dark:bg-white/[0.03] dark:text-gray-100 dark:focus:ring-blue-500/30 dark:empty:before:text-gray-500"
+ />
-
-
setAttachHover(true)}
- onMouseLeave={() => setAttachHover(false)}
- >
-
+ {/* 桌面端按钮行 */}
+
+
+
+ fal.ai 的文生图模式不支持 auto 参数>}
+ />
+
+
+
setAttachHover(true)}
+ onMouseLeave={() => setAttachHover(false)}
+ >
+
+
+
-
setSubmitHover(true)}
- onMouseLeave={() => setSubmitHover(false)}
- >
-
-
-
-
+
+ )}
+
>
diff --git a/src/hooks/useHintTooltip.ts b/src/hooks/useHintTooltip.ts
new file mode 100644
index 00000000..021e540a
--- /dev/null
+++ b/src/hooks/useHintTooltip.ts
@@ -0,0 +1,54 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { onDismissTooltips } from '../lib/tooltipDismiss'
+
+interface UseHintTooltipOptions {
+ enabled?: () => boolean
+ autoHideMs?: number
+ touchDelayMs?: number
+}
+
+export function useHintTooltip(options: UseHintTooltipOptions = {}) {
+ const { autoHideMs, touchDelayMs = 450 } = options
+ const [visible, setVisible] = useState(false)
+ const timerRef = useRef(null)
+ const enabledRef = useRef(options.enabled)
+ enabledRef.current = options.enabled
+
+ const clearTimer = useCallback(() => {
+ if (timerRef.current != null) {
+ window.clearTimeout(timerRef.current)
+ timerRef.current = null
+ }
+ }, [])
+
+ const hide = useCallback(() => {
+ setVisible(false)
+ clearTimer()
+ }, [clearTimer])
+
+ const show = useCallback(() => {
+ if (enabledRef.current && !enabledRef.current()) return
+ clearTimer()
+ setVisible(true)
+ if (autoHideMs != null) {
+ timerRef.current = window.setTimeout(() => {
+ setVisible(false)
+ timerRef.current = null
+ }, autoHideMs)
+ }
+ }, [autoHideMs, clearTimer])
+
+ const startTouch = useCallback(() => {
+ if (enabledRef.current && !enabledRef.current()) return
+ clearTimer()
+ timerRef.current = window.setTimeout(() => {
+ setVisible(true)
+ timerRef.current = null
+ }, touchDelayMs)
+ }, [touchDelayMs, clearTimer])
+
+ useEffect(() => () => { clearTimer() }, [clearTimer])
+ useEffect(() => onDismissTooltips(hide), [hide])
+
+ return { visible, show, hide, clearTimer, startTouch }
+}
diff --git a/src/index.css b/src/index.css
index 27b614ec..60cf5d0a 100644
--- a/src/index.css
+++ b/src/index.css
@@ -296,3 +296,31 @@ input[type="number"] {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35);
}
}
+
+.scrollbar-hide {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+.scrollbar-hide::-webkit-scrollbar {
+ display: none;
+}
+
+.animate-slide-up {
+ animation: slide-up 0.25s ease-out;
+}
+@keyframes slide-up {
+ from { transform: translateY(100%); }
+ to { transform: translateY(0); }
+}
+
+.pb-safe {
+ padding-bottom: max(1.25rem, env(safe-area-inset-bottom));
+}
+
+.animate-fade-in-up {
+ animation: fade-in-up 0.25s ease-out;
+}
+@keyframes fade-in-up {
+ from { opacity: 0; transform: translateY(4px); }
+ to { opacity: 1; transform: translateY(0); }
+}