From c9f6e6a8d16ef1f341179b51b61728c67354bef2 Mon Sep 17 00:00:00 2001 From: MarkLo Date: Sat, 13 Dec 2025 07:14:45 +0800 Subject: [PATCH] --- frontend/app/globals.css | 56 ++++- frontend/components/analysis/AnalysisForm.tsx | 203 +++++++++++------- 2 files changed, 175 insertions(+), 84 deletions(-) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index ddc7c8ba..27240478 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -121,6 +121,41 @@ } } +/* Safari mobile touch fixes */ +@layer base { + /* Fix for Safari iOS touch events on buttons and interactive elements */ + button, + [role="button"], + input[type="submit"], + input[type="button"], + .cursor-pointer { + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; + -webkit-touch-callout: none; + } + + /* Ensure form elements are properly interactive on iOS */ + select, + input, + textarea { + -webkit-appearance: none; + appearance: none; + -webkit-tap-highlight-color: transparent; + } + + /* Fix for Radix UI popover/select on iOS Safari */ + [data-radix-popper-content-wrapper] { + touch-action: auto !important; + } + + /* Ensure buttons inside forms work on iOS */ + form button[type="submit"] { + cursor: pointer; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; + } +} + /* Custom animations */ @keyframes fadeIn { from { @@ -186,7 +221,8 @@ /* Heartbeat animation for buttons */ @keyframes heartbeat { - 0%, 100% { + 0%, + 100% { transform: scale(1); } 10% { @@ -283,19 +319,19 @@ /* Gradient Backgrounds - Blue/Pink in light, Blue/Purple in dark */ .gradient-bg-primary { - background: linear-gradient(135deg, #3B82F6 0%, #EC4899 100%); + background: linear-gradient(135deg, #3b82f6 0%, #ec4899 100%); } .dark .gradient-bg-primary { - background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); + background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); } .gradient-bg-secondary { - background: linear-gradient(135deg, #60A5FA 0%, #F472B6 100%); + background: linear-gradient(135deg, #60a5fa 0%, #f472b6 100%); } .dark .gradient-bg-secondary { - background: linear-gradient(135deg, #60A5FA 0%, #A78BFA 100%); + background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); } .gradient-bg-accent { @@ -436,28 +472,28 @@ /* Gradient Text - Blue/Pink in light, Blue/Purple in dark */ .gradient-text-primary { - background: linear-gradient(135deg, #3B82F6 0%, #EC4899 100%); + background: linear-gradient(135deg, #3b82f6 0%, #ec4899 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .dark .gradient-text-primary { - background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); + background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .gradient-text-secondary { - background: linear-gradient(135deg, #60A5FA 0%, #F472B6 100%); + background: linear-gradient(135deg, #60a5fa 0%, #f472b6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .dark .gradient-text-secondary { - background: linear-gradient(135deg, #60A5FA 0%, #A78BFA 100%); + background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -635,5 +671,3 @@ box-shadow: 0 4px 20px 0 rgba(147, 51, 234, 0.2), inset 0 1px 0 0 rgba(255, 255, 255, 0.1); } - - diff --git a/frontend/components/analysis/AnalysisForm.tsx b/frontend/components/analysis/AnalysisForm.tsx index 0eb60df0..6c3bda85 100644 --- a/frontend/components/analysis/AnalysisForm.tsx +++ b/frontend/components/analysis/AnalysisForm.tsx @@ -52,10 +52,10 @@ const formSchema = z.object({ research_depth: z.number().int().min(1).max(5), quick_think_llm: z.string().min(1, "請選擇快速思維模型"), deep_think_llm: z.string().min(1, "請選擇深層思維模型"), - + // Market type selection: us=美股, twse=上市, tpex=上櫃/興櫃 market_type: z.enum(["us", "twse", "tpex"]), - + // Custom model names (when "custom" is selected) custom_quick_think_model: z.string().optional(), custom_deep_think_model: z.string().optional(), @@ -79,8 +79,8 @@ const formSchema = z.object({ .optional() .or(z.literal("")), embedding_api_key: z.string().min(1, "請輸入嵌入模型 API Key"), - alpha_vantage_api_key: z.string().optional().or(z.literal("")), // 選填 - finmind_api_key: z.string().optional().or(z.literal("")), // 選填 + alpha_vantage_api_key: z.string().optional().or(z.literal("")), // 選填 + finmind_api_key: z.string().optional().or(z.literal("")), // 選填 }); interface AnalysisFormProps { @@ -125,7 +125,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { const marketType = form.watch("market_type"); const isQuickThinkCustom = quickThinkLlm === "custom"; const isDeepThinkCustom = deepThinkLlm === "custom"; - + useEffect(() => { // Use async version to get decrypted API keys const loadSettings = async () => { @@ -133,27 +133,57 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { // For custom models, always use custom base URL and API key if (isQuickThinkCustom) { - form.setValue("quick_think_base_url", savedSettings.custom_base_url || ""); - form.setValue("quick_think_api_key", savedSettings.custom_api_key || ""); + form.setValue( + "quick_think_base_url", + savedSettings.custom_base_url || "" + ); + form.setValue( + "quick_think_api_key", + savedSettings.custom_api_key || "" + ); } else { - form.setValue("quick_think_base_url", getBaseUrlForModel(quickThinkLlm, savedSettings.custom_base_url)); - form.setValue("quick_think_api_key", getApiKeyForModel(quickThinkLlm, savedSettings)); - } - - if (isDeepThinkCustom) { - form.setValue("deep_think_base_url", savedSettings.custom_base_url || ""); - form.setValue("deep_think_api_key", savedSettings.custom_api_key || ""); - } else { - form.setValue("deep_think_base_url", getBaseUrlForModel(deepThinkLlm, savedSettings.custom_base_url)); - form.setValue("deep_think_api_key", getApiKeyForModel(deepThinkLlm, savedSettings)); + form.setValue( + "quick_think_base_url", + getBaseUrlForModel(quickThinkLlm, savedSettings.custom_base_url) + ); + form.setValue( + "quick_think_api_key", + getApiKeyForModel(quickThinkLlm, savedSettings) + ); } - form.setValue("embedding_base_url", savedSettings.custom_base_url || "https://api.openai.com/v1"); - form.setValue("embedding_api_key", savedSettings.custom_api_key || savedSettings.openai_api_key); - form.setValue("alpha_vantage_api_key", savedSettings.alpha_vantage_api_key || ""); + if (isDeepThinkCustom) { + form.setValue( + "deep_think_base_url", + savedSettings.custom_base_url || "" + ); + form.setValue("deep_think_api_key", savedSettings.custom_api_key || ""); + } else { + form.setValue( + "deep_think_base_url", + getBaseUrlForModel(deepThinkLlm, savedSettings.custom_base_url) + ); + form.setValue( + "deep_think_api_key", + getApiKeyForModel(deepThinkLlm, savedSettings) + ); + } + + form.setValue( + "embedding_base_url", + savedSettings.custom_base_url || "https://api.openai.com/v1" + ); + form.setValue( + "embedding_api_key", + savedSettings.custom_api_key || savedSettings.openai_api_key + ); + form.setValue( + "alpha_vantage_api_key", + savedSettings.alpha_vantage_api_key || "" + ); form.setValue("finmind_api_key", savedSettings.finmind_api_key || ""); }; - + loadSettings(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [quickThinkLlm, deepThinkLlm, isQuickThinkCustom, isDeepThinkCustom]); @@ -163,9 +193,13 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { const currentTicker = form.getValues("ticker"); // 只在用戶未修改預設值時才自動切換 const isTwStock = marketType === "twse" || marketType === "tpex"; - const isDefaultUsTicker = currentTicker === "NVDA" || currentTicker === "AAPL"; - const isDefaultTwTicker = currentTicker === "2330" || currentTicker === "2317" || currentTicker === "6488"; - + const isDefaultUsTicker = + currentTicker === "NVDA" || currentTicker === "AAPL"; + const isDefaultTwTicker = + currentTicker === "2330" || + currentTicker === "2317" || + currentTicker === "6488"; + if (isTwStock && isDefaultUsTicker) { form.setValue("ticker", marketType === "twse" ? "2330" : "6488"); } else if (marketType === "us" && isDefaultTwTicker) { @@ -189,31 +223,36 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { function handleSubmit(values: z.infer) { // Use custom model names if "custom" is selected - const finalQuickThinkLlm = values.quick_think_llm === "custom" - ? values.custom_quick_think_model || "" - : values.quick_think_llm; - - const finalDeepThinkLlm = values.deep_think_llm === "custom" - ? values.custom_deep_think_model || "" - : values.deep_think_llm; - + const finalQuickThinkLlm = + values.quick_think_llm === "custom" + ? values.custom_quick_think_model || "" + : values.quick_think_llm; + + const finalDeepThinkLlm = + values.deep_think_llm === "custom" + ? values.custom_deep_think_model || "" + : values.deep_think_llm; + // Validate custom model names - if (values.quick_think_llm === "custom" && !values.custom_quick_think_model) { + if ( + values.quick_think_llm === "custom" && + !values.custom_quick_think_model + ) { form.setError("custom_quick_think_model", { type: "manual", - message: "請輸入快速思維模型的完整名稱" + message: "請輸入快速思維模型的完整名稱", }); return; } - + if (values.deep_think_llm === "custom" && !values.custom_deep_think_model) { form.setError("custom_deep_think_model", { type: "manual", - message: "請輸入深層思維模型的完整名稱" + message: "請輸入深層思維模型的完整名稱", }); return; } - + const request: AnalysisRequest = { ...values, quick_think_llm: finalQuickThinkLlm, @@ -255,17 +294,18 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
{ANALYSTS.map((analyst) => { - const isSelected = field.value?.includes(analyst.value); + const isSelected = field.value?.includes( + analyst.value + ); return ( - +
{ const newValue = isSelected - ? field.value?.filter((v: string) => v !== analyst.value) + ? field.value?.filter( + (v: string) => v !== analyst.value + ) : [...(field.value ?? []), analyst.value]; field.onChange(newValue); }} @@ -284,9 +324,13 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { : "border-muted-foreground" )} > - {isSelected && } + {isSelected && ( + + )}
- {analyst.label} + + {analyst.label} +
@@ -318,20 +362,27 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { - + 🇺🇸 美股 - + 🇹🇼 台股上市 - + 🇹🇼 台股上櫃/興櫃 - - 選擇分析的股票市場 - + 選擇分析的股票市場 )} @@ -345,21 +396,23 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { 股票代碼 - - {marketType === "us" - ? "輸入美股代碼(例如:NVDA、AAPL)" + {marketType === "us" + ? "輸入美股代碼(例如:NVDA、AAPL)" : marketType === "twse" ? "輸入上市股票代碼(例如:2330、2317)" - : "輸入上櫃/興櫃股票代碼(例如:6488、5765)" - } + : "輸入上櫃/興櫃股票代碼(例如:6488、5765)"} @@ -376,7 +429,9 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { { - field.onChange(date ? format(date, "yyyy-MM-dd") : "") + field.onChange( + date ? format(date, "yyyy-MM-dd") : "" + ); }} placeholder="選擇分析日期" className="w-full" @@ -535,7 +590,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { Qwen: Flash - + {/* Custom Model */} Other(自訂模型) @@ -547,7 +602,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { )} /> - + {/* Custom Quick Think Model Input */} {isQuickThinkCustom && ( 自訂快速思維模型名稱 - + 請輸入完整的模型名稱(此模型將使用自訂端點) @@ -680,7 +732,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { Qwen: Flash - + {/* Custom Model */} Other(自訂模型) @@ -692,7 +744,7 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { )} /> - + {/* Custom Deep Think Model Input */} {isDeepThinkCustom && ( 自訂深層思維模型名稱 - + 請輸入完整的模型名稱(此模型將使用自訂端點) @@ -723,6 +772,14 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { className="w-full bg-gradient-to-r from-blue-500 to-pink-500 dark:from-blue-600 dark:to-purple-600 hover:from-blue-600 hover:to-pink-600 dark:hover:from-blue-700 dark:hover:to-purple-700 shadow-lg hover:shadow-xl transition-all animate-heartbeat" disabled={loading} size="lg" + style={{ + touchAction: "manipulation", + WebkitTapHighlightColor: "transparent", + }} + onClick={(e) => { + // Ensure touch events work on Safari mobile + e.currentTarget.blur(); + }} > {loading ? "執行分析中..." : "執行分析"}